#707 wrote generated start/stop files for the family picker. Swap those
for the downloaded preview bytes (voca is pair 05) and skip the catalog
ids in generate_sounds.py so they stay put.
* fix(settings): stop Test Dictation calling a missing model "no speech"
start_recognition now returns whether listening actually began. Test
Dictation checks that before starting its timer, so a missing or
auto-paused model gets a real error instead of "No speech detected".
* fix(settings): reconfigure Test Dictation against the live engine
UI matching config.json is not enough when speech_engine is still on
another engine or model size. Compare those too before the listen timer.
* fix(tests): keep settings_dialog lookups on one module
_load_settings_dialog() restored sys.modules but left
vocalinux.ui.settings_dialog pointing at the private reload.
Python 3.9/3.10 mock follows that attribute, so
test_whisper_list_and_delete patched a different copy than it
imported and the expanduser stub hid the real cache.
Point the package attribute back at the restored module, and
patch the imported module object in the deletion test.
Settings → Audio can pick start/stop cues from the shared catalog
(lift, flick, ember, step, voca, soft, chirp, scale, drop, glass, off).
New installs and any missing or unknown sound_effects.tone value use
voca. A saved id, including off, is left alone.
The old start_recording/stop_recording files stay on disk. They are
no longer the implicit default. Error cues still follow the existing
enable switch.
* fix(install): leftover review notes from #700 and #705
The scratch dir now holds pip logs and model download temps, so a failed
run has something to inspect. Dep, venv, leftover-process, and pip
failures use exit codes 2/3/4 instead of a blanket 1. --help says the
transcript is written during installation, and the tag hint is
--tag=<release>.
AGENTS.md no longer claims the hash-pinned exports are consumed (they
wait for phases 2/3/5). Docs add a just install line and
vocalinux[vosk] next to the PyPI commands. Settings points at
--engine=whisper. The whisper.cpp fallback installs ".[vosk]".
Fixes leftover review notes from #700 and #705.
Refs #701.
* fix(install): do not let pip build PyGObject from sdist
Fresh --auto --skip-system-deps died on pygobject-3.56.3 even with
distro python3-gi present. Install the other runtime deps from
pyproject.toml, then pip install --no-deps. Extra installs use the
extra's packages only, not pip install ".[extra]".
Fixes leftover review notes from #700 and #705.
Refs #701.
* fix(install): tolerate unset XDG_SESSION_TYPE under set -u
--auto with DISPLAY but no login session died on
XDG_SESSION_TYPE: unbound variable. The session probe now uses
${XDG_SESSION_TYPE:-}, ${WAYLAND_DISPLAY:-}, and ${DISPLAY:-}.
Phases 0-1 of #701. Drops dead deps, moves vosk to an optional extra, adds uv.lock and hash-pinned requirements exports, pins pywhispercpp to 1.5.0.
Closes#705. Refs #701.
Stacked bottom of #701. Replaces Makefile with justfile and hardens install.sh (strict mode, logging, HTTP probes, curl -f on model downloads, no v0.10.1-beta fallback).
Closes#700.
Replace the old traced app icon and tray SVGs with the approved family
mic geometry from VocaHQ/.github brand assets. Regenerate vocalinux.com
favicons and logo PNGs/WebPs from the same source. No version bump.
Per brand book §9 and §10.
TestTypedAccessors builds its ConfigManager with __new__ and, unlike
TestConfigManager, never patches CONFIG_DIR/CONFIG_FILE. That is fine
for the accessor tests, but test_whispercpp_advanced_persistence calls
save_config(), which writes the synthetic fixture from setUp over the
developer's real ~/.config/vocalinux/config.json — running the suite
silently wiped every personal setting. Caught by making the real file
read-only and running the suite with live logging; it is the only
write to the real path in the entire suite.
Redirect the paths to a temp directory for this test, the same way
TestConfigManager's setUp already does.
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
Both modal download paths ran _apply_settings_internal() on a worker
thread and then unconditionally marked the progress dialog complete.
Since _apply_settings_internal() swallows every exception internally,
a download that failed or was cancelled still ended with "Complete!",
the dedicated except blocks below (the "Download cancelled" and
Whisper-not-installed branches) were unreachable, and the swallowed
path built and ran a Gtk.MessageDialog from the download thread, off
the main loop.
Give _apply_settings_internal() a raise_errors flag and pass it from
the two thread call sites. Their handlers become reachable and already
report through the progress dialog via GLib.idle_add, so failures now
surface with the real reason and no Gtk work happens off the main
loop. Main-loop callers keep the old contract: swallow, show the error
dialog, return False.
* fix(whisper): use bundled GPU libs and skip software Vulkan devices
AppImage 0.15.0 shipped a CPU-only pywhispercpp wheel, so the Settings
GPU picker and host vulkaninfo never moved inference off the CPU. Rebuild
pywhispercpp with Vulkan in the AppImage, log the libraries that actually
load, skip llvmpipe in auto-select, and document how to verify.
Fixes#604
* fix(appimage): pin gcc/g++ for Vulkan pywhispercpp rebuild
Hosts that default cc/c++ to clang fail CMAKE's C++ test because clang
does not search gcc's libstdc++ path. Default the rebuild to gcc/g++.
Repo links, install one-liners, and release checks now use
github.com/VocaHQ/vocalinux. The site mentions VocaHQ without a redesign.
The Codeberg mirror, personal email, and X handle are unchanged.
Prefer 2ch before 1ch when PortAudio reports two or more input
channels. Intel SOF digital mics (the Raptor Lake device in #666)
often only support stereo at the PCM; opening them as converted
mono can abort with heap corruption right after capture starts.
Mono-only devices are still never probed with 2 channels (#567).
* fix(installer): gate util-linux-extra to Ubuntu 24.04+
util-linux-extra does not exist on Ubuntu 22.04 (package split landed in
24.04). Listing it unconditionally in APT_PACKAGES_UBUNTU breaks
install.sh on 22.04-based distros such as Linux Mint.
Fixes#526
* fix(installer): probe util-linux-extra via apt-cache
Replace Ubuntu VERSION_ID gate with apt-cache show probe (per review
feedback). Handles Ubuntu derivatives (Mint, Zorin) whose VERSION_ID
does not match the noble base where util-linux-extra exists.
Fixes#526
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* feat(settings): delete unused downloaded speech models
Add a Remove Model dropdown on the Speech Model page so whisper.cpp,
Whisper, and VOSK downloads can be deleted from Settings. The currently
selected model is marked in use and cannot be removed until you switch
away from it.
Fixes#650
* fix(settings): move unused model deletion below the picker
Unused downloads is its own group under the model info card. Only leftover
files appear, each with a Delete button. The group hides when empty.
* fix(settings): collapse unused downloads into an expander
Keep leftover models behind one row so a long list does not stretch the
Speech Model page. Expand to delete files one at a time; the list scrolls
once it grows past a few entries.
* fix(settings): keep leftover model list from clipping in the expander
Wrap the unused-downloads list so the expander can show a few rows and
scroll the rest, instead of painting only the first leftover.
* feat(settings): make language selector a searchable combobox
Turn the Speech Model language picker into a ComboBoxText with an
entry and GtkEntryCompletion so typing filters the 34+ language list
instead of scrolling the whole alphabet.
Fixes#652
* fix(settings): use ComboBoxText text/id column order for language search
Gtk.ComboBoxText stores display text in column 0 and the id in column 1.
Point EntryCompletion at the text column so typing "span" lists Spanish.
Add a horizontal separator between the sidebar Test Dictation
controls and the in-window Close button so Close reads as dialog
chrome rather than part of the testing section.
Fixes#651
Scoped IBus injection (#457) temporarily switches to the Vocalinux
engine but only restored the previous IBus engine, not the XKB map.
On X11 that left non-US layouts (e.g. Brazilian ABNT2) stuck on us
after dictation. Capture XKB before the switch and re-apply it in
the inject_text finally block (Wayland remains a no-op via #474).
Fixes#664
* feat(shortcuts): default to hold Right Alt push-to-talk on first install
Double-tap Ctrl conflicts with many desktop and app shortcuts, so new
installs now use hold Right Alt (Option) in push-to-talk mode. Existing
config.json values are preserved unchanged.
Updates README, user guide, AGENTS.md, first-run dialog, website copy,
and tests to match the new defaults.
* refactor(shortcuts): trim redundant default-change test and comments
Drop the fresh-install assertion that duplicated DEFAULT_CONFIG coverage,
remove the duplicated Right-Alt rationale from config_manager, and drop a
vacuous source-string check from the settings shortcuts test.
* fix(shortcuts): align install.sh seeds and preserve legacy toggle mode
install.sh still wrote ctrl+ctrl without mode, so merges with the new
push_to_talk default became hold-Ctrl. Seed right_alt + push_to_talk for
new installs, and migrate existing shortcut-only configs to mode=toggle
so prior behavior is unchanged.
* style(config): fix black formatting after shortcuts migration
* fix(shortcuts): preserve legacy defaults when shortcuts section is missing
Existing config files without a shortcuts section previously inherited
ctrl+ctrl toggle from DEFAULT_CONFIG. Pin those historical defaults on
upgrade so the new first-install Right Alt push-to-talk defaults do not
silently change behavior.
* docs(agents): unwrap hard-broken dictation control paragraph
Keep the AGENTS.md paragraph as a single line like the surrounding prose.
* feat(updates): notify when a newer GitHub release is available
Check the configured release channel shortly after startup and every six
hours. When an update exists, show a tray menu item and a green New badge
on Settings → About, plus one desktop notification per version, so users
can open release notes and update themselves.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* refactor(updates): slim UpdateMonitor after ponytail review
Drop unused APIs (tick/check_now/version injector/last_* state), integer-only
GLib scheduling, and the local demo launcher. Keep tray notify-send local to
avoid coupling to recognition_manager.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(updates): address Bugbot and harden notify persistence
Persist last_notified_version only after notify-send spawns successfully,
and sync the tray Update Available item from About-page checks without
re-notifying. Also merge latest main and cover the new paths in tests.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* test(tray): pin config mock for update-notification assertions
CI on 3.9/3.10 showed empty MagicMock set call history after a successful
notify-send spawn. Pin tray.config_manager to the test mock and record set()
via side_effect so persistence checks are reliable across Python versions.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(updates): clear pending About update on failed lookup
A failed About check hid the New badge but left _pending_update set, so
clearing settings search could revive the badge while status still showed
the failed lookup. Clear dialog pending state on failure (tray unchanged)
and restore the badge via _set_about_update_badge.
* fix(updates): ignore stale UpdateMonitor results after channel switch
Bind the channel for each background check and discard the callback when
the configured channel changed mid-flight, then re-check immediately —
matching About-page stale-channel handling so the tray cannot show the
wrong channel's update state.
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(clipboard): text-only reads and safe overlapping restore
Force wl-paste/xclip to request text MIME types so image/file
clipboards are not decoded and "restored" as corrupted text.
Stop treating xclip's "target not available" as an empty clipboard.
Coordinate overlapping ydotool pastes with a generation counter and a
pending restore target so a second paste within the 300ms window
restores the original pre-injection content, not intermediate text.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* refactor(clipboard): simplify restore ownership after ponytail review
Bump the restore generation only after a successful clipboard overwrite
so a failed copy cannot cancel an in-flight restore. Clear pending
target on paste failure unconditionally. Drop redundant state reads in
the restore thread and the artificial stale-restore unit test.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix: restore clipboard after ydotool clipboard-paste injection
When ydotool is used on Wayland to inject non-ASCII text (Arabic,
accented letters, CJK, etc.), the injection path copies the text to
the clipboard and simulates Ctrl+V. This permanently overwrites whatever
the user had on their clipboard, which is surprising and disruptive —
especially for users dictating in languages that always trigger this path.
This commit adds two changes:
1. `_read_clipboard()` — reads the current clipboard content using the
first available tool (wl-paste, xclip, xsel), returning None if the
clipboard is empty or unreadable.
2. `_inject_via_clipboard_paste()` now saves the clipboard before
overwriting it and schedules a background thread to restore it 300 ms
after the Ctrl+V paste completes. The delay gives the target application
time to process the paste before the clipboard content changes.
The previous note "there is no safe race-free way to restore" was
accurate in the strict sense, but a best-effort restore after a short
delay is far better than never restoring, and covers all practical use
cases (the user is not expected to trigger another paste within 300 ms of
a dictation commit).
Fixes the UX issue where every dictation commit silently destroyed the
user's clipboard content.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* tests: add unit tests for clipboard save/restore (PR #588)
15 tests covering _read_clipboard() (tool fallback chain, error handling,
UTF-8 decoding) and the restore behaviour in _inject_via_clipboard_paste
(content restored after delay, no restore on empty clipboard or paste
failure, daemon thread assertion).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qg9NpTr6QaeXEyEBc5fvvz
* style: apply ruff formatting to test_clipboard_restore.py
Fixes CI lint failure — ruff format requires line length ≤ 88 chars.
No logic changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qg9NpTr6QaeXEyEBc5fvvz
* style: apply black formatting to fix CI lint check
ruff format and black disagree on some style choices; the CI runs black.
No logic changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qg9NpTr6QaeXEyEBc5fvvz
* fix: use text=True in _read_clipboard and respect copy_to_clipboard setting
Two issues:
1. _read_clipboard() called subprocess.run without text=True, meaning
stdout was bytes and .decode() was called. Existing tests that mock
subprocess.run return stdout="" (str), causing AttributeError on
.decode(). Switch to text=True + encoding/errors params so stdout is
always a str and no .decode() is needed.
2. When the user has copy_to_clipboard=true, inject_text() copies the
dictated text to clipboard after injection. The restore thread in
_inject_via_clipboard_paste was firing 300ms later and overwriting
that with the old content, silently breaking the setting. Skip the
restore when _should_copy_to_clipboard() returns True.
Tests: adds test_no_restore_when_copy_to_clipboard_setting_enabled,
updates all _read_clipboard mock stdout to str (was bytes).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qg9NpTr6QaeXEyEBc5fvvz
* fix: Python 3.9 compat and cover restore-failure branch
Use Optional[str] instead of str | None for _read_clipboard return type
so the code runs on Python 3.9 (PEP 604 union syntax requires 3.10+).
Add test_restore_failure_is_handled_gracefully to exercise the else-branch
inside the _restore() closure, covering the two previously uncovered lines
reported by Codecov.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qg9NpTr6QaeXEyEBc5fvvz
* fix: restore clipboard on paste failure and handle empty clipboard
Fix 1 (comment #1): when Ctrl+V simulation fails the clipboard was already
overwritten with dictated text but the original content was silently lost.
Now restore it immediately in the except block before returning False.
Fix 3 (comment #3): wl-paste and xclip signal an empty clipboard with a
non-zero exit code, causing _read_clipboard() to return None and skip the
restore — leaving dictated text in the clipboard forever. Detect the
"Nothing is copied" / "target STRING not available" stderr patterns and
return "" so the restore path can clear the clipboard back to empty.
Tests added for both behaviours; existing test updated to assert the
correct post-fix behaviour for paste failure.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qg9NpTr6QaeXEyEBc5fvvz
* test: cover paste-failure branch when clipboard is unreadable
Add test for the case where Ctrl+V fails and previous_clipboard is None
(clipboard was unreadable), ensuring the restore is correctly skipped.
Closes the 1298->1300 branch gap reported by Codecov.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qg9NpTr6QaeXEyEBc5fvvz
* test: cover wl-paste fallback on X11 (XWayland) path
Add a test for the `not host_is_wayland and shutil.which("wl-paste")`
branch in _read_clipboard(), which was showing as yellow/red on Codecov.
Simulates an X11 session with wl-paste available (XWayland environments).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qg9NpTr6QaeXEyEBc5fvvz
* fix: proper clipboard clear and race guard on restore
Fix 5 (comment #5): add _clear_clipboard() that uses each backend's
native clear command (wl-copy --clear, xsel --clear, xclip with empty
input) instead of passing "" to _copy_to_clipboard(). Called when
previous_clipboard is "" to truly clear rather than set an empty offer.
Fix 6 (comment #6): before restoring, read the clipboard inside the
_restore() thread and skip if it no longer holds the injected text —
protecting against the user manually copying something else during the
300ms window.
Tests added for _clear_clipboard() per backend and for both new
restore-path behaviours. Existing restore tests updated to use
side_effect lists since _read_clipboard() is now called twice.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qg9NpTr6QaeXEyEBc5fvvz
* test: cover remaining _clear_clipboard() branches for Codecov
Add tests for:
- xclip path in _clear_clipboard() (line 931)
- health-check skip (line 911)
- exception fallthrough to next tool (line 947)
- _clear_clipboard() called immediately when paste fails with empty
previous clipboard (line 1354)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qg9NpTr6QaeXEyEBc5fvvz
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
Document that CUDA-backed pywhispercpp always uses device 0, and
point to CUDA_VISIBLE_DEVICES or a Vulkan build as workarounds.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(whisper): use CUDA device 0 when pywhispercpp is CUDA-backed
Vulkan GPU enumeration on hybrid laptops lists the iGPU as GPU0 and the
dGPU as GPU1, but CUDA ordinals always place the first NVIDIA GPU at 0.
Auto mode and the settings UI were passing Vulkan indices into CUDA
pywhispercpp, which silently fell back to CPU inference.
Detect the actual pywhispercpp backend before selecting gpu_device and
map CUDA installs to device 0. Vulkan-backed installs keep Vulkan indices.
* test(whisper): cover CUDA gpu_device selection branches for codecov
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* fix(ibus): restore engine after register_component teardown
Stopping the Vocalinux IBus engine process destroys its
register_component registration. IBus then runs check_global_engine(),
which only searches register_engine_list and treats XML engines like
xkb:es::spa as missing, clearing GlobalEngine even when Vocalinux was
never selected. Capture a restorable engine before teardown and
reselect it afterward so dead keys keep working after quit.
Fixes#558
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(ibus): wait for engine exit before restoring GlobalEngine
Adversarial review of #643 found that stop_engine_process() returned
right after SIGTERM, so switch_engine() could race ahead of IBus
check_global_engine() and still leave No global engine. Wait for the
process to exit (SIGKILL fallback), retry engine restore, capture a
shutdown fallback during prepare_engine, and re-apply XKB after the
post-teardown engine switch.
Addresses review of #558 / #643.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(ibus): prefer live engine over stale warmup on quit
Bugbot on #643: stop() restored the warmup-cached engine even when the
user had switched input source afterward, and inject_text refused to
refresh that cache once set. Prefer the live non-Vocalinux engine at
quit; fall back to the cache only when stuck on vocalinux/unavailable.
Always refresh the shutdown fallback from inject_text.
Also cover SIGKILL teardown escalation and restore-retry failure paths
to lift patch coverage.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(appimage): ship transitive GI typelibs for non-Debian hosts
The 0.15.0 AppImage pruned girepository down to a partial allowlist and
dropped xlib/Dbusmenu/GModule. On Ubuntu the missing typelibs fell back
to /usr/lib/x86_64-linux-gnu/girepository-1.0; on openSUSE/Fedora that
path does not exist, so Gtk failed to import and startup reported
missing GTK3/AppIndicator (#585).
Keep linuxdeploy's typelib extras, seed the full transitive set, bundle
AppIndicator/Notify shared libs, and smoke-test GI imports with host
typelibs hidden.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* style(test): format appimage packaging regression test
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(ci): avoid libappindicator/ayatana apt conflict
Ubuntu packages libappindicator3-1 and libayatana-appindicator3-1
conflict. Rely on gir1.2-* to pull a single implementation and keep
libdbusmenu/libnotify explicit for bundling.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(ci): install gir1.2-dbusmenu-glib for AppImage typelib seed
Dbusmenu-0.4 is a required transitive typelib for AppIndicator; without
the GIR package the build fails the required-typelib check.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(appimage): require only one AppIndicator typelib variant
Runtime accepts AppIndicator3 or AyatanaAppIndicator3. Treat them as
alternates in the AppImage typelib check so single-stack build hosts
do not fail require_all.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(appimage): prefer Ayatana after rebase onto main
Align AppImage typelib seeding and GI smoke order with main's
Ayatana-first tray preference from #621/#638.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(appimage): accept lowercase AyatanaAppindicator3 typelib
Match tray_indicator.py's three-step fallback so AppImage builds and
GI smoke succeed on hosts that only ship AyatanaAppindicator3-0.1.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(main): accept lowercase AyatanaAppindicator3 in dep check
Align check_dependencies with tray_indicator.py and the AppImage GI
smoke path so a lowercase-only typelib host does not fail at startup.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* docs(packaging): prefer Ayatana AppIndicator in Fedora/Arch hints
Follow-up to #621: align docs, AUR depends, package map, dependency
check messages, and troubleshooting copy with the Ayatana-first install
path so manual installs and AUR users get a working KDE tray icon.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* style(main): black-format Ayatana install hint lines
CI black --check failed on the multi-line logger.error calls for the
Fedora/Arch package hints.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* Fix missing KDE tray icon by preferring Ayatana AppIndicator
Fedora and Arch installs pull libappindicator-gtk3, Canonical's original
AppIndicator library that hasn't had a real release since ~2013. It loads
fine and Indicator.new_with_path()/set_status() run without raising, but
it never registers a StatusNotifierItem with KDE's StatusNotifierWatcher,
so the tray icon silently never shows up on Plasma. Confirmed on Fedora 44
/ Plasma 6.7 by checking RegisteredStatusNotifierItems over D-Bus directly
before and after the fix.
The actively maintained Ayatana fork registers correctly and is already
what the Debian, Ubuntu, openSUSE and Gentoo branches of install.sh
prefer. Reorder the gi import in tray_indicator.py to try
AyatanaAppIndicator3 first and fall back to the legacy AppIndicator3
typelib only if neither Ayatana variant is available.
Update install.sh so Fedora and Arch installs pull the Ayatana package
too, via a shared install_preferred_appindicator() helper that falls back
to the legacy package if Ayatana isn't in the repos (e.g. RHEL/CentOS
without EPEL).
* Add tests for the AppIndicator/Ayatana import fallback chain
The reordered try/except in tray_indicator.py wasn't exercised by the
existing tests: they replace gi.repository with a MagicMock, which
auto-creates any attribute on access and so can never raise the
ImportError the fallback chain is built to catch.
Add a small test class that swaps in a plain object exposing only a
chosen subset of AppIndicator symbols, so importing anything else raises
a real ImportError, and verify each of the three branches (Ayatana,
lowercase Ayatana variant, legacy AppIndicator3) resolves correctly.
* Retry Ayatana install even when legacy AppIndicator is already present
install_preferred_appindicator() previously skipped the Ayatana install
whenever either package was already present, so re-running install.sh
on an existing Fedora/Arch setup with libappindicator-gtk3 from a prior
install would never pick up Ayatana, leaving the KDE tray broken for
exactly the users this fix targets. Now it only short-circuits when
Ayatana itself is installed, tries Ayatana first regardless of legacy
presence, and only falls back to a warning about the already-installed
legacy package if the Ayatana install fails.
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* docs(web): refresh screenshots for v0.15 sidebar settings UI
Recapture product and settings gallery shots against the searchable
sidebar settings dialog, keeping the previous asset filenames and
exact pixel dimensions with wallpaper padding around each window.
* fix(web): correct screenshot semantics after adversarial review
Keep the existing transcription and tray images because the replacements did not
show their claimed flows. Populate the Logs dialog and unlock Advanced settings
so those refreshed images demonstrate the features described by the gallery.
* fix(web): retake screenshots as square crops with uniform padding
Resize settings dialogs to a near 1:1 window, capture with ~28px wallpaper
padding on all sides, open About from the tray menu, and skip redoing the
awkward XFCE system-tray asset.
* docs(web): retake v0.15 screenshots on GNOME (light/dark)
Window-only captures from Ubuntu/GNOME at 960x720 for settings and
logs, with matching dark variants under screenshots/dark/. Replace the
old Shortcuts shot with Performance to match the sidebar.
* feat(web): theme-aware light/dark screenshot switching
Serve paired GNOME captures and swap them with the site theme via
dark:hidden / hidden dark:block. Replace Shortcuts gallery entry with
Performance to match the v0.15 sidebar.
* docs(web): refresh screenshots after About moves into Settings
Rebase onto main brought the in-app update checker: About is now a
sidebar settings page. Recapture light/dark GNOME panels at 960x720 and
update gallery copy/metadata accordingly.
* docs: use single screenshot source for README and website
Point README images at web/public/screenshots so product shots live in
one place, drop the unused resources/screenshots tree (including the old
demo video and about-updates set), and replace the missing shortcuts
shot with performance.
* docs(web): refresh light/dark screenshots and product grid
Replace the screenshot set with fresh captures for both themes, and
lay out the three Product shots in a single equal-height row.
* fix(web): align screenshot metadata, sizes, and theme loading
Restore dictation copy for the 00-transcription asset, set width/height
from the recaptured PNGs, preload only the light next/image twin when
priority is set, and eager-load both theme variants by default so dark
mode does not flash empty on first toggle.
* fix(web): show full settings screenshot without crop
Use object-contain so the homepage settings dialog fits the frame
instead of object-cover clipping the sidebar.
git push --tags fails when a tag already exists on Codeberg (e.g.
v0.15.0 from a prior sync or retag). GitHub is the source of truth for
this read-only mirror, so force-push tags to keep them aligned.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* feat(settings): add in-app update checker with stable/nightly channels
Move About into the Settings sidebar and check GitHub Releases for updates.
Stable uses /releases/latest; Nightly follows nightly-YYYY-MM-DD tags.
Remove the standalone About dialog and wire tray About to Settings.
Fixes#183
* docs(readme): update About screenshot caption for Settings page
* fix(settings): address update-checker review and About polish
Discard stale in-flight results when the update channel changes, page
through GitHub releases for nightly tags, restore the About logo next
to the title, and replace the em dash in the app description.
* docs(screenshots): add About update-checker state captures
Host PR review screenshots on the branch so GitHub can render them
from raw.githubusercontent.com (Cursor artifact URLs 403 in PR bodies).
* fix(updates): recognize .devYYYYMMDD nightlies and clear Open style
Parse CI nightly versions like 0.14.2.dev20260802+sha when checking for
updates, and drop the suggested-action style from Open after a failed
GitHub lookup.
* fix(audio): filter unsafe virtual devices that crash capture
This is a defensive fix for #624 heap corruption seen when PortAudio opens PipeWire pseudo-devices such as DeepFilterNet, default, pipewire, paplay, monitor, null, dummy, or filter-chain sources. The root cause is not fully proven, but avoiding explicit capture from these unsafe virtual rows should keep dictation on real microphones while still allowing System Default through device_index=None.
If this still crashes with a physical microphone selected, please confirm with the exact physical device name and audio stack details so the capture path can be narrowed further.
Fixes#624
* fix(settings): sync audio device fallback with raw name restore
Address Bugbot review on #629: match legacy "(default)" saved names and
clear stale config/engine state when a filtered device falls back to
System Default.
* fix(audio): keep system default capture off pseudo device indices
Stop converting System Default to an explicit PortAudio index in the
mic test, and fall back to host default capture when every enumerated
input is filtered as unsafe.
* test(audio): expect system-default fallback on reconnect
* test(audio): cover system-default fallback paths for capture
* feat(tray): allow disabling missing tray warning dialog
Fixes#620
* test(tray): stabilize missing-watcher opt-out coverage
Default the D-Bus ListNames mock to include StatusNotifierWatcher so
TrayIndicator construction no longer opens the warning dialog during
setUp, and bind the don't-show-again checkbox test to the module Gtk mock.
* test(tray): make missing-watcher opt-out tests deterministic
* chore(release): prepare v0.15.0
Bump version across app, web, AUR, and Flatpak metainfo. Document GitHub
release-notes rules, refresh README/UPDATE/INSTALL/USER_GUIDE for the 0.15
series (sidebar settings, AppImage, auto-pause/keepalive, Vulkan device
selection), and align website product surfaces with shipped features.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* docs(release): fold latest main into v0.15.0 notes
Include dictation capitalization/trailing-space, ibus-wayland injection,
IBus teardown, and settings notice polish landed on main after the initial
release prep rebase.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* docs(release): include Bluetooth mic crash fix in v0.15.0 notes
Fold #599 (PortAudio Bluetooth SCO probe heap corruption) into README,
UPDATE, changelog, AppStream, and release history after rebasing onto main.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* docs(release): include language catalog expansion in v0.15.0 notes
Fold #616 (Hungarian and expanded speech languages, Fixes#565) into
README, UPDATE, changelog, AppStream, PRODUCT.md, and release history
after rebasing onto latest main.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* docs(release): fold settings footer and language follow-ups into v0.15.0
Include #618 sidebar dictation footer, #619 Custom Shortcut Record/Set
restore, and #617 English (India) Whisper language mapping after rebasing
onto latest main.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(settings): restore custom shortcut Record/Set controls
Gtk.Widget.show_all() is a no-op while no_show_all is True, so the
contextual Custom Shortcut row never appeared after selecting it in the
searchable settings UI. Clear the flag before showing and restore it on
hide.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(settings): sync custom row when reverting shortcut separators
Selecting Custom Shortcut then a combo group separator restored the
preset id but left Record/Set visible. Reuse _sync_shortcut_selection_ui
so combo and custom-row visibility stay aligned.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(settings): clear stale Record/Set hint on separator revert
Selecting Custom Shortcut sets a temporary info hint; clicking a combo
separator already restored the row visibility but left that hint in
place. Refresh the mode description after syncing.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(languages): map en-in to Whisper code en
English (India) used the catalog id as the Whisper language argument,
which is invalid. Resolve Whisper/whisper.cpp/remote_api codes via the
SUPPORTED_LANGUAGES whisper field so en-us and en-in both become en.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* test(languages): cover whisper language resolver fallbacks
Exercise unknown catalog ids and legacy en-us fallback paths so
codecov/patch covers resolve_whisper_language fully.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
Expose Hungarian and other high-demand Whisper languages in Settings/CLI,
wire official VOSK models where Alphacephei ships them, keep CLI choices
derived from SUPPORTED_LANGUAGES, and update the languages marketing page
with honest per-engine support.
Fixes#565
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(audio): prevent Bluetooth mic probe heap corruption
Open PortAudio streams only once when negotiating capture format, and
always stop_stream() before close(). Separate channel/rate probing was
aborting on Bluetooth SCO devices (e.g. FreeBuds) with malloc corruption.
Fixes#567
* refactor(audio): open capture stream once instead of probe-close-reopen
Adversarial review of the first fix found it still closed the probe
stream and immediately reopened the device — the same rapid open/close
pattern blamed for the Bluetooth SCO heap corruption, just fewer cycles.
_open_capture_stream() now returns the successfully negotiated stream
and all three callers (mic test, recording, reconnect) use it directly,
so each capture session performs exactly one PortAudio open. Also drops
the dead known_working_rate parameter.
* test(audio): cover negotiation fallback paths for codecov patch
Add tests for mic-test, recording, and reconnect fallback branches when
_open_capture_stream returns no stream, plus Bluetooth settle-on-failure
and empty-read reconnect cleanup. Brings patch diff coverage above 80%.
* feat(dictation): append trailing space after completed transcription
Fix cross-session glueing ("Hello.This") for push-to-talk and toggle
by appending a trailing space to each injected segment instead of relying
on in-session leading-space memory that is cleared on IDLE.
Adds an optional Settings toggle (default on) and preserves legacy
leading-space behavior when disabled. Newlines from voice commands are
left without a trailing space.
Fixes#605
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(dictation): honor trailing-space setting from disk live
Read append_trailing_space from config.json on each injection so the
Settings toggle applies without restart. main() and TrayIndicator each
own a ConfigManager, so an in-memory read missed Settings writes — same
pattern as TextInjector.copy_to_clipboard.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* test(dictation): align capitalize tests with trailing-space default
Update auto-capitalize expectations to include the default trailing
space, and add edge-case coverage for legacy mode, newlines, empty
input, and config-read failures so patch coverage clears 80%.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* feat: auto-capitalize sentences after punctuation
Add capitalize_sentences() function that uppercases the first letter of
dictated text and after sentence-ending punctuation (., !, ?) followed by
whitespace. Preserves URLs, decimals, and abbreviations without trailing space.
- Config option text_injection.auto_capitalize (default: true)
- Settings toggle in General tab to opt out
- 17 tests covering edge cases
Fixes#553
* style: black-format auto-capitalize helpers for CI lint
Fixes the Lint Python check failure on the auto-capitalize PR.
* refactor: make auto-capitalize Vosk-only with clear UI indication
- Gate auto-capitalize feature to Vosk engine only (Whisper outputs proper casing)
- Update settings tooltip to explain Vosk-only behavior
- Update subtitle to show '(Vosk only)' for user clarity
* test(main): cover Vosk-only auto-capitalize injection path
Add callback wiring tests for enabled/disabled auto-capitalize on Vosk
and the Whisper skip path so codecov patch coverage clears the 80% bar.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* refactor(settings): move Auto-Capitalize to Recognition tab
Auto-capitalize is dictation output post-processing, not application
lifecycle behavior, so it belongs alongside Voice Commands rather than
with autostart/clipboard settings in General.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
Drop the thick left accent strip on .info-box helpers so they read as
muted flat surfaces with a thin full border, matching the preference
cards instead of looking like callout banners.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(ibus): keep engine teardown correct when parent destroy fails
`do_destroy` passed `super().do_destroy` to `_handle_engine_destroy`.
PyGObject binds that vfunc to the GType rather than to the instance, so
calling it with no arguments raises:
TypeError: IBus.Object.destroy() takes exactly 1 argument (0 given)
This fired on every engine teardown. The exception escaped
`_handle_engine_destroy` before its return, so neither
`_active_instance = ...` nor `_focus_event.clear()` in the caller ran,
leaving a destroyed engine registered as active with focus still set --
the two values `_ibus_ping_status` (#523) reads to decide commit
readiness.
Two changes:
- Bind the parent destroy to the instance, falling back to
`IBus.Object.destroy(self)` only when the no-arg call raises TypeError,
so this keeps working if PyGObject binds the vfunc correctly later.
- Guard the parent destroy call so no failure there can skip teardown.
Fixes#606
* refactor(ibus): extract parent-destroy workaround into a testable helper
Codecov flagged the new code as uncovered (36% patch, below the 80%
target). The whole `if IBUS_AVAILABLE:` branch of `do_destroy` is
unreachable from the suite, which only ever exercises that method with
IBUS_AVAILABLE False -- the single line it replaced was already
uncovered on main for the same reason.
Move the workaround into a module-level `_invoke_parent_destroy`, the
same pattern `_handle_engine_destroy` already uses to make `do_destroy`
testable, and bind it with functools.partial at the call site.
Behaviour is unchanged. Coverage of the branch goes from 0 to all but
the one-line partial, and three tests now pin the actual contract:
pass-through when the binding is correct, retry with the instance on the
GType-binding TypeError, and no masking of other exceptions.