* 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
* feat(web): add v0.10.1 updates and 5 new SEO pages
## New SEO Pages (5)
- /vs-nerd-dictation/ - Vocalinux vs Nerd Dictation comparison
- /whisper-model-guide/ - Whisper model sizes explained
- /voice-typing-vscode/ - How to dictate in VS Code on Linux
- /writers/ - Voice dictation for writers on Linux
- /gnome-kde/ - Voice typing on GNOME vs KDE Plasma
## Existing Page Updates
- homepage: Update JSON-LD featureList with v0.10.1 features, add new FAQs, link new pages in footer
- troubleshooting: Add suspend/resume recovery, tray resources, settings dialog fixes
- shortcuts: Add sound effects toggle, left/right modifier distinction, PTT improvements
- wayland: Add keyboard layout preservation, clipboard fallback note
- compare: Add engine switching safety notes
- faq: Add suspend/resume and keyboard layout FAQs
- alternatives: Link to new Nerd Dictation comparison
- seo-subpage-shell: Add new pages to navigation
- sitemap: Add 5 new URLs
Fixes#257 (SEO expansion)
* fix(web): add theme toggle to subpages, fix dark mode button contrast, simplify footer nav
- Add light/dark theme toggle to SeoSubpageShell header (all subpages)
- Fix duplicate useState/useEffect imports causing build error
- Fix unclosed div in header markup
- Add dark:text-black to CTA buttons across all 15 subpages for contrast on bright green bg
- Add dark:text-black to footer CTA button in SeoSubpageShell
- Simplify subpage footer to mainNavCategories (5 condensed categories) instead of full navCategories
* fix(web): fix list items stacking, add all new pages to nav, restructure footer
- Fix inline-flex → flex on all li elements across 13 pages so bullet points
always stack on separate lines instead of running together horizontally
- Add all 5 new SEO pages to subpage nav mainNavCategories (Whisper Models,
For Writers, VS Code, GNOME vs KDE under Features/Use Cases; new Compare
dropdown with vs Nerd Dictation, Alternatives, Offline, Open Source)
- Add /vs-nerd-dictation/ to homepage footer Learn column
- Restructure homepage footer from 2 crowded columns into 4 lean categorised
columns (Get Started, Learn, Use Cases, Support) — responsive 2-col on
mobile/tablet, 5-col on desktop
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
Add cmake, libcairo2-dev, and dynamic libgirepository version detection
to fix installation failures on Pop!_OS (Cosmic DE) and newer Ubuntu.
Three issues addressed:
1. cmake: Required by pywhispercpp to build whisper.cpp from source.
Added to all distro package lists (apt, dnf, pacman, zypper, emerge,
apk, xbps, eopkg).
2. libcairo2-dev: Required to build pycairo. Was present in the Debian
package list but missing from the Ubuntu list, which Pop!_OS uses.
3. libgirepository version: Ubuntu 24.04+ and Pop!_OS Cosmic+ ship
libgirepository-2.0-dev instead of libgirepository1.0-dev. Added
runtime detection to select the correct package, mirroring the
existing version-aware handling already in place for Debian.
Reported-by: DavidHoenisch
Ref: https://github.com/jatinkrmalik/vocalinux/discussions/378
* 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
Update release runbook steps to match current installer and workflow behavior, including main/install.sh usage, website lockfile/version checks, valid install sanity checks, and web build verification.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* 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
* test(coverage): improve test coverage from 57% to 80%+
Add 30 new test files with 861 additional test cases to improve
overall test coverage from 57% to 80%+ (statement coverage).
Key modules covered:
- recognition_manager.py: 43% → 69% (audio device detection, transcription,
model download, engine initialization, buffer management)
- text_injector.py: improved coverage for X11/Wayland text injection,
environment detection, clipboard fallback, shortcut injection
- ibus_engine.py: 22% → 73% (engine lifecycle, text injection, socket server)
- evdev_backend.py: 11% → 78% (device detection, key event handling)
- pynput_backend.py: 43% → 76% (start/stop, key handling, callbacks)
- keyboard_backends/__init__.py: 54% → improved (backend creation, env detection)
- whispercpp_model_info.py: 32% → improved (GPU detection, model recommendations)
- about_dialog.py, first_run_dialog.py, main.py, single_instance.py,
common_types.py, config_manager.py, autostart_manager.py: various improvements
All 1433 tests pass with proper test isolation to prevent mock pollution
between test modules.
* test(quality): audit and fix test quality issues
Code review audit identified and fixed the following issues:
- Remove bare try/except blocks that swallow all errors (7 tests fixed
in recognition_manager_deep, replaced with proper assertions or deleted)
- Delete source code inspection tests that read .py files as strings
instead of testing behavior (deleted test_about_dialog_coverage.py and
test_first_run_dialog_coverage.py entirely - ~35 useless tests removed)
- Add meaningful assertions replacing weak assertIsNotNone/assertIsInstance
checks (common_types_coverage, ibus_engine_coverage, keyboard_backends_extra)
- Fix tests that mock the system under test instead of its dependencies
(text_injector_extra now mocks subprocess.run instead of injection methods)
- Add missing assertions to tests with no verification
(ibus_engine focus_out, log window info, copy to clipboard)
- Verify download tests check requests.get was called with correct URLs
(coverage_boost download tests)
- Fix pytest warning by renaming test_audio_input import to avoid
pytest collecting it as a test function
All 1356 tests pass with 0 failures and 0 warnings.
Statement coverage: 80.3% (3257/4054 lines covered).
* style(tests): fix black formatting and isort import ordering
Auto-format all new test files with black and isort to pass CI lint checks.
* fix(tests): resolve CI hangs and rename test files for clarity
- Fix CI hangs by properly mocking gi/GTK/IBus in all test files
- Fix sys.modules pollution in recognition manager tests by using tmp_path
- Add pytest-timeout to prevent future hangs
- Rename 28 test files to use descriptive, consistent names mapping to source modules
* fix(tests): resolve Python 3.13 CI failures
- Fix _load_stdlib_module() in test_recognition_manager_downloads.py to
handle stdlib modules that are packages (directories with __init__.py)
rather than single .py files. In Python 3.13, zipfile became a package,
causing FileNotFoundError during test collection (exit code 2).
- Fix invalid escape sequence '$' in test_cross_distro_compatibility.py
by using a raw string. Python 3.13 reports this as SyntaxWarning and
Python 3.14 will make it a SyntaxError.
* fix(tests): use direct import for zipfile instead of _load_stdlib_module
The _load_stdlib_module approach fails on Python 3.13 where zipfile is a
package (zipfile/__init__.py). Loading it via spec_from_file_location with
a custom name breaks relative imports inside the package (e.g.
'from ._path import ...' resolves to '_stdlib_zipfile._path' which doesn't
exist).
Simply import zipfile at module level before any test mocks can pollute
sys.modules — this is safe because the module-level code runs during test
collection, before any test fixtures or patches are applied.
* fix(tests): mock gi/GTK in conftest to prevent CI hangs
On CI, real PyGObject is installed and module-level imports in vocalinux
source files (ibus_engine, tray_indicator, settings_dialog, etc.) trigger
GTK initialization or IBus daemon connections that hang indefinitely in
headless environments. pytest-timeout's signal-based timeout cannot
interrupt these C-level blocking calls.
Fix by mocking gi and gi.repository in conftest.py (which runs before any
test module is collected), ensuring all test files are protected regardless
of whether they remember to mock individually.
Also add timeout-minutes: 10 to the CI test job as a safety net so that
any remaining hangs fail fast instead of running indefinitely.
* fix(tests): unconditionally mock gi in conftest for CI
The previous 'if gi not in sys.modules' guard was ineffective on CI
because PyGObject is installed via pip, so the real gi module is already
loaded. This caused GTK initialization and IBus daemon connections that
hang indefinitely in headless CI environments.
Now unconditionally replace gi and gi.repository with mocks during test
collection, regardless of whether the real package is installed.
* fix(tests): reduce timeout to 10s and use thread-based timeout
Use timeout_method=thread instead of signal (default) so that
pytest-timeout can interrupt C-level blocking calls (GTK operations,
socket accepts, subprocess waits) that the signal-based method cannot
handle. Also reduce per-test timeout from 30s to 10s since no legitimate
unit test should take longer, and hanging tests were consuming the entire
CI job timeout budget.
* fix(tests): set global socket timeout to prevent blocking on CI
Tests in test_ibus_engine.py create real Unix sockets with daemon threads
that block on socket.accept(). When tests fail or timeout, these threads
remain blocked indefinitely, preventing pytest from completing. The
thread-based timeout catches the hang but cannot terminate the blocking
accept() call.
Set socket.setdefaulttimeout(5) in conftest.py so all socket operations
(accept, connect, recv) will raise socket.timeout after 5 seconds instead
of blocking forever.
* fix(tests): add cleanup fixture for IBus socket server threads
Add autouse fixture that stops any leftover VocalinuxEngine socket server
threads after each test. Tests that trigger _start_socket_server() spawn
daemon threads blocking on socket.accept() that persist after the test
completes, eventually causing pytest-timeout to kill the process.
The cleanup fixture sets _server_running=False and closes the server
socket, which unblocks the accept() call and allows the daemon thread
to terminate.
* fix(tests): reduce socket timeout to 0.1s to avoid slowing tests
The 5-second socket timeout caused socket operations in daemon threads
to block for 5 seconds per iteration before timing out, significantly
slowing the test suite on CI. Reduce to 0.1s so blocking socket calls
fail fast without noticeably impacting test execution time.
* fix(tests): use 2s socket timeout and increase CI timeout to 15min
The 0.1s socket timeout was too aggressive and caused legitimate test
socket operations to fail. Use 2s which is long enough for test sockets
to work but short enough to unblock daemon threads.
Increase CI job timeout to 15 minutes to accommodate the socket timeout
overhead from daemon threads that retry every 2 seconds.
* fix(tests): block known-blocking daemon threads to prevent CI hangs
Instead of using socket timeouts (which either break tests or are too
slow), prevent specific known-blocking daemon threads from starting:
- ibus_engine.server_thread: blocks on socket.accept()
- evdev_backend._monitor_devices: blocks on select.select()
These threads are identified by their target function name and skipped
during Thread.start(). The thread is marked as started/stopped so
join() calls in cleanup code don't raise RuntimeError.
Test-created daemon threads (e.g. mock socket servers) are unaffected
since they use different target function names.
* fix(tests): patch Thread.join for Python 3.13 compatibility
Python 3.13 changed threading internals — join() now uses _handle.join()
which raises 'thread not started' for threads that were never actually
started. Instead of trying to fake internal state, patch join() to be a
no-op for threads that were skipped by our _safe_thread_start guard.
This fixes the 2 remaining test_evdev_backend_ext failures on Python 3.13.
- Remove setup.py: redundant with pyproject.toml, had drifted deps
(missing pywhispercpp, psutil, evdev; no platform markers; stale
Python 3.12 max). pip has supported pyproject.toml since v21.3 (2021).
- Remove build-website.sh: broken script referencing non-existent
fix-paths.sh. Web deploy is handled by npm scripts and GitHub Actions.
- Clean up CI triggers: remove setup.py and setup.cfg references from
distro-test-matrix.yml and unified-pipeline.yml path filters.
* docs: improve project description with narrative intro
- Replace the one-liner README intro with a more compelling narrative
that highlights key differentiators: offline, open source, multi-engine,
GPU-accelerated, and privacy-first
- Update version.py __description__ to a more descriptive one-liner
* docs: replace all em-dashes with hyphens/commas/colons
Remove all em-dash (—) characters from README.md, replacing them
with contextually appropriate alternatives (hyphens, commas, colons).
- unified-pipeline.yml: replace blanket .github/workflows/** glob with
specific .github/workflows/unified-pipeline.yml in both web and python
filters, so editing nightly.yml or release.yml no longer triggers the
full lint/test/build matrix
- distro-test-matrix.yml: add paths filter at workflow trigger level so
the 4-container distro matrix only runs when installer, scripts,
packaging, or version files change (previously ran on every push/PR
including doc-only and web-only changes)
Saves CI minutes on documentation, website, and unrelated workflow edits.
* feat(docs): add Voca Ecosystem section to README and website
Add cross-project ecosystem shoutouts linking VocaLinux, VocaMac, and
VocaWin. README gets a table with platform, project, website, GitHub,
and status columns. Website gets animated cards with platform logos,
tech badges, status indicators, and links.
Uses simpleicons CDN for Apple logo and inline SVG for Windows logo
(simpleicons CDN does not serve a Windows icon).
* feat(web): UI refinements — terminal install boxes, contrast fixes, wider card grids
- Replace CodeBlock components with terminal-style command boxes (traffic-light dots, copy button, dark bg)
- Fix dark mode contrast: add dark:text-black to badges and buttons on green backgrounds
- Header: replace GitHub icon with filled yellow Star icon next to star count
- Hero buttons: rename and reorder (Install Now, We're Open Source, Use Windows or Mac?)
- Engine section: widen container from max-w-5xl to max-w-7xl to match features
- Ecosystem section: add VocaLinux 'You are here' card, widen from max-w-4xl to max-w-7xl
- Fix Windows icon color to official Microsoft Fluent blue (#0078D4)
- Remove unused CodeBlock import
* feat(web): link sub-pages, reorder cards, restyle hero, update FAQ, remove em-dashes
- Add href prop to FeatureCard: 6 tiles link to /offline/, /use-cases/,
/gpu-acceleration/, /shortcuts/, /wayland/, /for-developers/
- Engine section: reorder to Whisper > whisper.cpp (center) > VOSK,
move 'Compare all engines' to centered link below cards
- whisper.cpp card keeps 'GPU acceleration' link
- FAQ answers: convert to ReactNode with inline links to sub-pages,
fix 6 stale answers (distros, requirements, languages, shortcuts,
voice commands, engine switching)
- Hero buttons: GitHub-dark 'We're Open Source', ghost 'Use Windows
or Mac?'
- Ecosystem: reorder to VocaMac > VocaLinux (center) > VocaWin,
change badge from 'You are here' to 'Beta'
- Remove all em-dashes (9 occurrences)
The install.sh on main now dynamically resolves the latest release tag
via GitHub API (merged in PR #313), so pinning to a specific release tag
in install commands is no longer needed — and was causing users to run
the old hardcoded installer.
Update all curl install command URLs across docs, code, and website from
versioned tags (v0.9.0-beta, v0.8.0-beta) to main/install.sh:
- README.md (4 URLs + note text)
- docs/INSTALL.md (2 URLs + note text)
- docs/UPDATE.md (2 URLs)
- docs/DISTRO_COMPATIBILITY.md (1 URL)
- tests/distro-testing-checklist.md (1 URL)
- src/vocalinux/main.py (1 URL)
- web/src/app/page.tsx (remove dynamic release fetch, hardcode main)
When running via `curl | bash`, stdin (fd 0) is the pipe — not the
terminal. The script was setting INTERACTIVE_MODE='ask' and then trying
to `read` from the pipe, which returned EOF/garbage, defaulting to
interactive mode, which then immediately errored because there was no TTY.
Fix: if stdin is a pipe but /dev/tty is accessible, do `exec < /dev/tty`
to redirect stdin to the real terminal so user prompts work correctly.
If there is truly no terminal (headless/CI), fall back to automatic mode
without erroring.
Also fix the misleading error message which suggested running the exact
same curl|bash command that just failed.
* chore(release): prepare v0.9.0-beta
- Bump version from 0.8.0-beta to 0.9.0-beta
- Update README What's New section with new features and bug fixes
- Update all install script URLs to v0.9.0-beta
- Add v0.9.0-beta What's New section to docs/UPDATE.md
- Update SECURITY.md supported versions (0.9.x supported, 0.8.x dropped)
- Update website schema softwareVersion and nav badge to 0.9.0
- Update website featureList with new sound effects and clipboard features
- Add v0.9.0-beta entry to changelog page
- Update web/package.json version to 0.9.0
- Add v0.9.0-beta row to RELEASE_PROCESS.md version history
* docs(readme): add contributors avatar grid via contrib.rocks
* feat(about): add Changelog link to About dialog
* Revert "feat(about): add Changelog link to About dialog"
This reverts commit 15e0984f516caccb14a0609e96d5fb6870ea247e.
The IBus injection path had an early return that skipped the clipboard
copy logic. Store the result, copy to clipboard if enabled, then return.
Also add xclip and wl-clipboard to install.sh for all supported distros
so clipboard tools are available out of the box.
Add a single toggle switch to enable/disable sound effects in the Audio
settings. This provides a simpler alternative to full sound customization
while covering the most common use case.
- Add sound_effects.enabled config option (default: true)
- Add toggle switch in Audio settings tab
- Add helper methods to ConfigManager
- Update play_*_sound() to check enabled state
- Add tests for the new functionality
Closes#283
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* fix: improve Debian/pipx installation experience
- Enhanced error messages in check_dependencies() to provide distro-specific
installation commands (Ubuntu/Debian, Fedora, Arch Linux)
- Added explicit pipx guidance: install system packages BEFORE pipx install
- Updated error message to use gir1.2-ayatanaappindicator3-0.1 for Debian
- Added comprehensive pipx installation section to DISTRO_COMPATIBILITY.md
with step-by-step instructions for all major distributions
* fix: add display availability check to prevent GTK crash on Debian
The original PR only added documentation and improved error messages.
This commit adds the actual fix for issue #288.
Root cause: On some systems (especially Debian), GTK widgets can be
created before the display connection is properly established, causing
the crash: gtk_widget_get_scale_factor: assertion 'GTK_IS_WIDGET (widget)' failed
Fix: Add check_display_available() function that verifies Gdk.Display
is available before any GTK widgets are created. This provides a clear
error message instead of a cryptic GTK crash.
Fixes#288
* fix: add clipboard fallback for Wayland compositors without virtual keyboard support
Fixes#297
On KDE Plasma Wayland, the compositor doesn't support the virtual keyboard
protocol that wtype requires. When both wtype and xdotool fallback fail,
text injection was completely broken.
Changes:
- Add _fallback_to_clipboard() method that tries wl-copy, xclip, and xsel
- When all injection methods fail, copy text to clipboard so user can paste
- Play success sound when clipboard fallback succeeds (text is recoverable)
- Improve error messages with KDE-specific guidance and ydotool instructions
For KDE Plasma Wayland users, the recommended solution is:
1. Install ydotool: sudo apt install ydotool
2. Enable daemon: sudo systemctl enable --now ydotoold
Alternatively, clipboard fallback will copy text for manual pasting.
* style: fix black formatting
* fix: wrap clipboard fallback in try/except to handle test mocks
* feat(clipboard): add configurable always-copy-to-clipboard feature
Enhanced the clipboard functionality beyond just a fallback mechanism:
- Renamed _fallback_to_clipboard to _copy_to_clipboard (more general)
- Added _should_copy_to_clipboard() to read user preference from config
- After successful text injection, copies to clipboard if setting enabled
- Still falls back to clipboard on injection failure regardless of setting
- Added 'copy_to_clipboard' setting to config (default: true)
- Added toggle switch in Settings > General for user control
This provides better UX by:
1. Allowing users to reference pasted text later via clipboard history
2. Providing a backup when injection partially works
3. Enabling easy pasting into apps where injection might not work
Fixes#297
* feat(clipboard): show notification when text copied as fallback
When text injection fails but clipboard succeeds, show a desktop
notification telling the user to paste with Ctrl+V. This ensures
users know their text was saved and can be recovered.
Reset last_injected_text when recognition transitions to LISTENING state,
ensuring the first dictation segment in each session is not prefixed with
a space. Also simplify the space-prepending logic: always add a separator
space between consecutive segments (not just after punctuation), and strip
incoming text as a safety net against whisper tokenizer artifacts.
Allow users to bind voice commands to specific modifier key sides (e.g.,
Right Ctrl only), preventing conflicts with common left-side shortcuts
like Alt+Tab or Ctrl+C.
- Expand SUPPORTED_SHORTCUTS from 3 to 9 entries (either/left/right)
- Add SHORTCUT_GROUPS for grouped display in settings UI
- Implement side-aware key matching in pynput and evdev backends
- Update settings dialog with grouped shortcut selector
- Add comprehensive tests for side-specific shortcuts
Closes#306
On app restart, the shortcut mode (toggle vs push-to-talk) was not being
passed to KeyboardShortcutManager during initialization in TrayIndicator.
The backend always defaulted to 'toggle' mode regardless of user config,
causing push-to-talk shortcuts to not bind correctly on startup.
Fixes#296
* feat(shortcuts): add push-to-talk mode as alternative to toggle
This commit implements push-to-talk functionality (Issue #252) as an
alternative to the existing double-tap toggle mode:
- Add keyboard shortcut mode setting (toggle vs push_to_talk)
- In push-to-talk mode: hold key to speak, release to stop
- Update both pynput and evdev backends to support press/release callbacks
- Add UI controls in settings dialog for mode selection
- Maintain backward compatibility with existing toggle mode as default
Closes#252
Related: Closes#185 (superseded by push-to-talk approach)
* fix(shortcuts): remove duplicate code blocks that broke settings dialog
The previous push-to-talk implementation had duplicate code blocks
in tray_indicator.py and settings_dialog.py that caused the settings
dialog to fail silently. This commit cleans up the duplicate code
while preserving the push-to-talk functionality.
Fixes issue where settings dialog would not open.
* fix(ui): add set_subtitle method to PreferenceRow class
The PreferenceRow class was missing a set_subtitle method, which is called by
_update_shortcut_ui_for_mode when switching between toggle and push-to-talk
modes. This caused an AttributeError that silently crashed the settings dialog
during initialization.
Also stores subtitle_label as instance variable for later updates.
* fix(keyboard): remove duplicate code and fix mode handling
Critical fixes for keyboard backends:
- evdev_backend.py: Removed duplicate __init__ and _handle_key_event methods
- pynput_backend.py: Removed duplicate __init__, _on_press, _on_release methods
- Both backends now properly handle toggle and push-to-talk modes
Fixed first_run_dialog.py:
- Added do_response() override to properly close dialog
- Added explicit destroy() call in show_first_run_dialog()
Fixed tray_indicator.py:
- Added _unregister_keyboard_callbacks() method
- Updated _setup_keyboard_shortcuts() to properly clean up old callbacks
- Updated update_shortcut() to properly handle mode switching
Fixed settings_dialog.py:
- Added set_subtitle() method to PreferenceRow class
- Updated UI text for push-to-talk mode
- Fixed _on_shortcut_changed to properly pass mode to callback
All 547 tests pass locally.
* feat(shortcuts): improve mode switching and add mode-aware display names
Key improvements:
- Add SHORTCUT_MODE_DISPLAY_NAMES for mode-specific shortcut names
- Add get_shortcut_display_name() function for mode-aware display
- Fix restart_with_shortcut() to properly save and restore all callbacks
- Fix update_shortcut() to properly handle mode changes
- Fix _setup_keyboard_shortcuts() to clear old callbacks before registering new ones
- Ensure only one type of callback is active at a time
This fixes issues where:
- Old callbacks remained registered after mode switch
- Single-tap triggered action after switching from push-to-talk to toggle
- Display names weren't appropriate for the selected mode
* fix(shortcuts): stabilize live mode switching callback lifecycle
* test(shortcuts): cover callback reset on mode transitions
* fix(shortcuts): remove super preset and migrate legacy config
* test(shortcuts): update coverage for super preset removal