* 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
Add a test for the for/else warning path in _setup_engine() introduced
in PR #361: when all 15 socket-readiness poll attempts exhaust without
SOCKET_PATH appearing, a WARNING is logged and activation is attempted
anyway. The new test mocks time.sleep to avoid real delays and asserts
the exact retry count (15) and log message.
Fixes Codecov patch-coverage gap reported on #361.
* 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.
* 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.
* 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
* 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.
* 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.
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)
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>
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
* 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.
* 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
* 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
* 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.
* 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.
* 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>
The command processor was adding a leading space when processing action
commands with text after them (e.g., 'delete that text' -> ' text').
This was causing the first speech to always have an unwanted leading space.
Changes:
- Modified command_processor.py to only add space when there's text BEFORE
the action command, not at the beginning of the transcription
- Updated test cases to reflect the correct behavior without leading spaces
Fixes leading space issue on first speech.
IBus sends Unicode directly via commit_text(), bypassing keyboard layout
issues entirely. This provides the same reliable text injection on X11
that we already have on Wayland.
Changes:
- Add X11_IBUS desktop environment enum
- Prefer IBus on both X11 and Wayland (before falling back to xdotool)
- Add tests for X11 IBus preference and fallback behavior
This supersedes the layout-switching approach in PR #206, which only
worked on X11 and had overhead from layout switching.
Closes#199
* fix(test): add missing mocks for IBus engine test
The test_init_auto_activate test was missing mocks for:
- install_ibus_component
- start_engine_process
Without these mocks, the test would fail in CI where IBus is not
installed, as _setup_engine() would fail early and never reach
switch_engine().
* style: fix black formatting in test_ibus_engine.py
* feat(text-injection): add IBus engine for Wayland text injection
Add a custom IBus input method engine for reliable text injection on
Wayland, solving keyboard layout issues with xdotool/wtype.
Key features:
- IBus proxy engine that passes through normal keyboard input
- Text injection via Unix socket IPC
- User-level component installation (no sudo required)
- Auto-reactivation when user switches keyboard layouts
- Graceful fallback when IBus is unavailable
Changes:
- New ibus_engine.py with VocalinuxEngine and IBusTextInjector
- TextInjector now prefers IBus on Wayland for best compatibility
- Add IBus GObject bindings to system dependencies in install.sh
- Add comprehensive tests for IBus engine
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* security: secure IBus socket with permissions and peer verification
- Set directory to 0o700 (owner-only access)
- Set socket to 0o600 (owner-only read/write)
- Add SO_PEERCRED verification to reject connections from other users
This provides defense-in-depth: even if directory permissions are
bypassed, the socket server verifies the connecting process UID.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: address PR review feedback for IBus text injection
- Fix infinite loop in socket server by adding _server_running flag
- Use sys.executable instead of hardcoded python3 for venv compatibility
- Also inherit environment (env=os.environ.copy()) for venv deps
- Fix component XML to use sys.executable in <exec> tag
- Replace unsafe pgrep with PID file tracking and /proc verification
- Integrate TextInjector.stop() in TrayIndicator._quit() for cleanup
- Remove unused is_engine_registered import
- Fix redundant hasattr check to use proper None comparison
- Auto-detect and update stale IBus component XML on startup
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: AntoineMontane <monnet.antoine@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Bump version from 0.6.0-beta to 0.6.1-beta with installation improvements:
Version Updates:
- src/vocalinux/version.py: 0.6.0-beta → 0.6.1-beta
- install.sh: Default tag and help text updated
- web/package.json: 0.6.0 → 0.6.1
- web/src/app/layout.tsx: schema.org softwareVersion updated
- web/src/app/page.tsx: version badge updated
Documentation Updates:
- README.md: New 'What's New' section for v0.6.1-beta
- docs/UPDATE.md: Added v0.6.1-beta release notes with new features
- docs/INSTALL.md: Version references updated
- docs/RELEASE_PROCESS.md: Example commands updated
- tests/distro-testing-checklist.md: Version references updated
- src/vocalinux/main.py: Error message updated
New Features in v0.6.1-beta:
- Interactive backend selection for whisper.cpp (GPU vs CPU)
- Enhanced welcome message with testing instructions
- Simplified install commands (tag-based URLs)
- Better GPU support with proper library detection
Bug Fixes:
- Fixed bash arithmetic with set -e
- Fixed TTY detection for interactive mode
- Auto-install git if missing
- Fixed GPU installation commands
Changed install commands to download install.sh directly from the release
tag instead of from main branch with --tag parameter. This ensures:
1. The install script version matches the release being installed
2. No version mismatch between script and code
3. Simpler, cleaner install commands
Changes:
- install.sh: Updated error message to use v0.6.0-beta URL
- README.md: All install commands now use v0.6.0-beta/install.sh
- docs/INSTALL.md: Updated quick install command
- docs/UPDATE.md: Updated update command examples
- docs/RELEASE_PROCESS.md: Updated example command
- src/vocalinux/main.py: Updated error message install command
- tests/distro-testing-checklist.md: Updated download URL
- web/src/app/page.tsx: Now uses dynamic tag in URL (not main)
- .github/workflows/release.yml: Release notes use tag-based URL
Before:
curl .../main/install.sh | bash -s -- --tag=v0.6.0-beta
After:
curl .../v0.6.0-beta/install.sh | bash
Fixes version consistency between install script and installed code.
* feat(speech): add whisper.cpp with Vulkan support for GPU acceleration
Add whisper.cpp as a new speech recognition engine with the following features:
- Vulkan GPU acceleration support (AMD, Intel, NVIDIA)
- Fallback to CUDA for NVIDIA GPUs
- CPU-only mode with automatic thread optimization
- Support for all model sizes: tiny, base, small, medium, large
- Hardware detection to recommend best model/backend
- Models downloaded from Hugging Face (ggml format)
- Updated UI to show compute backend info in settings
- pywhispercpp as the Python binding library
- whisper_cpp as the new default engine
Changes:
- New whispercpp_model_info.py with model metadata and hardware detection
- Updated recognition_manager.py with whisper.cpp init and transcription
- Updated settings_dialog.py with whisper.cpp UI support
- Updated config_manager.py with whisper_cpp_model_size config
- Updated main.py with whisper_cpp CLI option
- Updated pyproject.toml with pywhispercpp dependency
- Updated install.sh with vulkan-tools for GPU detection
Closes#104
* style: fix black and isort formatting issues
* test: update config manager test for whisper_cpp default engine
* feat(install): interactive mode with 3 engine choices and whisper.cpp as default
Major changes to install.sh:
- Interactive mode is now the DEFAULT (not --auto)
- Added --auto flag for non-interactive automatic installation
- New --engine flag to specify engine directly
- Interactive mode shows 3 engine options with visual boxes:
1. whisper.cpp (recommended) - Vulkan/CUDA/CPU support
2. whisper (OpenAI) - PyTorch-based, NVIDIA-only
3. VOSK - lightweight option
- Hardware detection with recommendations:
- Detects Vulkan support (AMD/Intel/NVIDIA)
- Detects NVIDIA CUDA
- Checks available RAM
- Recommends best engine based on hardware
- Updated help text with new options and examples
- Removed old --with-whisper, --whisper-cpu, --no-whisper flags
- whisper.cpp is now the default engine for automatic mode
* feat(uninstall): kill running vocalinux processes before uninstall
* fix(whisper.cpp): auto-download model, use all CPU cores, hide unavailable engines
Three fixes for whisper.cpp implementation:
1. Auto-download model during install
- Added install_whispercpp_model() function to install.sh
- Downloads ggml-tiny.bin (~39MB) from Hugging Face
- Runs automatically when whisper.cpp engine is selected
2. Use all CPU cores for transcription
- Added n_threads parameter to Model initialization
- Uses multiprocessing.cpu_count() to get all cores
- Significantly improves transcription speed
3. Hide unavailable engines from dropdown
- Added get_available_engines() function to detect installed engines
- Settings dialog now only shows engines that are actually installed
- Falls back to first available engine if current not available
Fixes user-reported issues with whisper.cpp integration.
* ui(settings): display whisper.cpp correctly and increase dialog height
- Changed engine display name from 'whisper_cpp' to 'whisper.cpp'
- Added format_engine_display_name() helper function
- Dialog now uses 75% of screen height for better visibility
- More options visible by default without scrolling
* revert(settings): remove display name change, keep dialog height increase
- Reverted engine display name change (was breaking engine selection)
- Kept dialog height at 75% of screen for better visibility
* style: fix black and isort formatting for whisper.cpp logging
* style: fix black formatting in settings_dialog.py
* ci: fix Fedora 39 container package installation
Add dnf cache update and fallback with --nogpgcheck for Fedora containers
that may have stale or missing package metadata.
Add testing infrastructure and documentation improvements.
**Task 12: Add pkg-config to Core Dependencies**
- Added pkg-config check at the start of install_system_dependencies()
- Added pkg-config to all distribution package lists
- Provides distro-specific install instructions if pkg-config is missing
- Critical for library detection (GI_TYPELIB_PATH, PortAudio, etc.)
**Task 13: Update README with Distro Info**
- Updated Requirements section with tested distributions
- Added link to DISTRO_COMPATIBILITY.md
- Listed officially supported distros: Ubuntu 22.04+, Debian 11+, Fedora 39+, Arch, openSUSE
- Mentioned experimental support for Gentoo, Alpine, Void, Solus
**Task 14: Create Test Matrix for CI**
- New workflow: .github/workflows/distro-test-matrix.yml
- Tests on multiple container images: Ubuntu 22.04, 24.04, Debian 12, Fedora 39
- Runs dependency checker on each distro
- Tests Python imports and bash syntax validation
- Uses continue-on-error for experimental distro support
**Task 15: Integration Testing Checklist**
- New file: tests/distro-testing-checklist.md
- Comprehensive checklist for testing on new distributions
- Covers: Pre-install, Installation, Post-install, Functionality, Performance, Cleanup
- Includes quick reference commands
- Desktop environment and application-specific testing sections
- Template for submitting test results
Part of cross-distribution compatibility improvements (Phase 4 of 5).
Related to #157
Removes the _escape_text method and its usage since text is passed to
subprocess.run() via list arguments, not shell strings. When using
subprocess with a list (without shell=True), arguments are passed directly
to the command without shell interpretation, making shell escaping
unnecessary and actually harmful.
This fixes the bug where apostrophes and other special characters were
typed with backslash escapes (e.g., "Let\'s" instead of "Let's").
Changes:
- Remove _escape_text() method from TextInjector class
- Remove escaped_text variable from inject_text() method
- Pass text directly to _inject_with_xdotool() and _inject_with_wayland_tool()
- Update docstrings to remove references to escaped text
- Update tests to verify text is NOT escaped when passed to subprocess
Closes#119
* feat(shortcuts): add customizable keyboard shortcuts via GUI
Implement configurable keyboard shortcuts for toggling voice recognition,
allowing users to choose between Ctrl+Ctrl, Alt+Alt, Shift+Shift, or
Super+Super (Windows key) double-tap shortcuts.
Changes:
- Add shortcuts configuration section to settings dialog with dropdown
- Update keyboard backends (pynput and evdev) to support configurable
modifier keys for double-tap detection
- Add SUPPORTED_SHORTCUTS, SHORTCUT_DISPLAY_NAMES constants in base module
- Update KeyboardShortcutManager to accept and pass shortcut configuration
- TrayIndicator now reads shortcut from config on startup
- Add comprehensive tests for shortcut configuration and parsing
The default shortcut remains Ctrl+Ctrl for backward compatibility.
Changes take effect after app restart (noted in UI).
Closes#16
* feat(shortcuts): enable live shortcut switching without app restart
Add restart_with_shortcut() method to KeyboardShortcutManager that stops
the listener, updates the shortcut configuration, and restarts with the
new shortcut. The settings dialog now applies shortcut changes immediately
with visual feedback indicating success.
- Add restart_with_shortcut() to KeyboardShortcutManager
- Add update_shortcut() to TrayIndicator for exposing live updates
- Update SettingsDialog to use callback for immediate shortcut changes
- Show green success message when live switching succeeds
- Add 7 new tests for restart_with_shortcut functionality
* fix: use Python 3.8 compatible context manager syntax
Convert parenthesized context manager (Python 3.10+) to nested with
statements for Python 3.8 compatibility in test_keyboard_shortcuts.py
* test: add comprehensive tests for UI dialog redesigns
- Add tests for AboutDialog (show_about_dialog function and class)
- Add tests for LoggingDialog (CSS, filtering, actions, toast notifications)
- Update SettingsDialog tests for instant-apply pattern (Close-only buttons)
- Use source code inspection to verify method existence with GTK mocks
- All 382 tests passing
* fix(test): rewrite UI dialog tests to use source inspection
Replace GTK mocking with source code inspection approach to fix
CI failures on Python 3.8 and 3.10. The new approach reads source
files directly and verifies method definitions, CSS classes, and
expected code patterns exist, avoiding Python version-specific
module caching issues with patched imports.
* feat(ui): redesign settings dialog with modern GNOME HIG styling
Completely redesign the settings dialog to follow GNOME Human Interface
Guidelines with a modern, card-based preference page layout:
UI/UX Improvements:
- Add PreferencesGroup and PreferenceRow custom widgets mimicking
libadwaita's AdwPreferencesPage pattern
- Implement proper visual hierarchy with card-style grouped sections
- Add comprehensive CSS stylesheet with theme-aware colors
- Use proper spacing, border-radius, and typography following HIG
- Add smooth reveal animations for settings applied notifications
- Improve status indicators with semantic colors (success/warning/error)
Technical Changes:
- Replace flat grid layout with scrollable content area
- Use ListBox rows for better accessibility and keyboard navigation
- Add proper Gdk version requirement to prevent version conflicts
- Consolidate model info display into a unified info card
- Improve code organization with dedicated section builder methods
Visual Features:
- Card-like preference groups with subtle borders and shadows
- Proper row hover states for interactive feedback
- Status color coding: green for success, yellow for warnings, red for errors
- Cleaner legend display for model download status
- Modern flat button styling for auxiliary actions
All 328 existing tests continue to pass.
* refactor(settings): simplify to Close-only button pattern
Remove Apply/Cancel buttons in favor of pure instant-apply UX:
- Settings now apply immediately when changed (auto-apply)
- Single Close button follows modern GNOME Settings pattern
- Remove redundant _revert_settings() and _show_settings_applied_message()
- Update docstring to reflect instant-apply design
This resolves the UX inconsistency where settings auto-applied
but also had Apply/Cancel buttons that were confusing.
* feat(ui): redesign Logs and About dialogs with GNOME HIG styling
Logs Dialog:
- Modern card-based filter bar with grouped controls
- Theme-aware log level colors (works in light/dark mode)
- Toolbar with icon buttons for Copy, Export, Clear actions
- Toast notifications for user feedback
- Close-only button pattern (consistent with settings)
About Dialog:
- Extract to separate about_dialog.py module
- Use standard Gtk.AboutDialog (GNOME recommended)
- Clean credits section with contributors and tech stack
- Remove emojis for cleaner professional look
- Better organized credit sections
Both dialogs now follow the same design language as the
Settings dialog redesign.
The test_check_dependencies_missing_appindicator test was failing because
it expected check_dependencies() to return False when AppIndicator3 is
missing. However, the code now supports AyatanaAppIndicator3 as a fallback.
Split the test into two cases:
- test_check_dependencies_missing_appindicator_with_ayatana_fallback:
Tests that when AppIndicator3 is missing but AyatanaAppIndicator3
is available, check_dependencies() returns True (fallback works)
- test_check_dependencies_missing_both_appindicators:
Tests that when both AppIndicator3 and AyatanaAppIndicator3 are
missing, check_dependencies() returns False
* 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: add Wayland support for keyboard shortcuts using evdev backend
This commit adds multi-backend support for keyboard shortcuts, enabling
them to work on Wayland sessions where pynput does not work.
**Changes:**
- New `keyboard_backends/` package with modular backend system
- `base.py`: Abstract base class for keyboard backends
- `pynput_backend.py`: Existing pynput implementation (X11/XWayland)
- `evdev_backend.py`: New evdev implementation (X11 & Wayland)
- `__init__.py`: Backend factory with auto-detection
- Updated `keyboard_shortcuts.py`:
- Refactored to use backend system
- Auto-detects session type (X11 vs Wayland)
- Selects appropriate backend automatically
- Provides helpful error messages when shortcuts unavailable
- Added `evdev>=1.6.0` to dependencies (Linux only)
- Comprehensive test updates:
- 21 tests pass covering all backends
- Tests for session detection
- Tests for backend factory
- Tests for permission hints
**User Impact:**
- Keyboard shortcuts now work on Wayland when user is in 'input' group
- Clear error messages guide users to fix permission issues
- Backward compatible with X11 (pynput backend)
**Setup for Wayland:**
```bash
sudo usermod -a -G input $USER
# Log out and log back in
```
Related to #80
* style: fix flake8 issues in evdev_backend.py
- Remove unused import 'categorize'
- Remove unused variable 'dev'
* style: fix remaining flake8 issues in tests
- Remove unused 'time' import
- Remove unused 'result' variable
- Remove unused 'backend_cls' variable
- Break long line to stay under 100 characters
* style: apply black and isort formatting to pass CI
* feat: auto-configure Wayland keyboard shortcuts during installation
Automatically detect Wayland sessions during installation and add the user
to the 'input' group required for evdev-based keyboard shortcuts.
This eliminates the need for users to manually configure permissions
after installation on Wayland systems.
* fix: escape commit message in Detect Changes step to handle special characters
The commit message was being directly inserted into the bash comparison,
which broke when the message contained special characters like parentheses.
Fix by assigning to a variable first.
* chore: drop unrelated installer/web changes
* fix: fall back to evdev backend
* style: format keyboard shortcut imports
* fix: Lazy GTK imports for better pip/pipx compatibility
- Move GTK-dependent imports (tray_indicator) inside main() function
- Import after check_dependencies() so users get helpful error messages
- Update ui/__init__.py to not import tray_indicator at module level
- Update vocalinux/__init__.py to not import GTK-dependent modules
- Improved error messages for pip/pipx users with missing GTK typelibs
- Bump version to 0.2.1-alpha
This fixes the 'Namespace AppIndicator3 not available' error that
pip/pipx users encounter because PyGObject can't find system typelibs.
Closes#47
* test: simplify settings dialog tests
* style: fix lazy import ordering
* fix: Update tests to include language parameter
- Add language to parse_arguments tests
- Update mock expectations for SpeechRecognitionManager to include language
- Fixes test failures after language support merge
* fix: Update tests for language support and fix None argument handling
- Fix test_parse_arguments_defaults to expect None for model/engine/language
- Add language_combo mock to settings_dialog tests
- Add language attribute to dialog mock setup
- Include language in get_selected_settings mock return values
- Update mock_config_manager to include language in return value