Commit Graph
136 Commits
Author SHA1 Message Date
Jatin K MalikandGitHub 4f2a91055e feat(tray): allow disabling missing tray warning dialog (#628)
* 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
2026-08-02 19:41:22 -07:00
Jatin K MalikandGitHub 2152dbb90d fix(whispercpp): skip unsupported context_params on pywhispercpp 1.4 (#626)
* fix(whispercpp): skip unsupported context_params on pywhispercpp 1.4

Fixes #625

* test(whispercpp): cover context_params signature inspection failure
2026-08-02 19:41:12 -07:00
Jatin K MalikandGitHub ced8620e69 fix(injection): stop typing test during wtype probe (#627)
Fixes #622
2026-08-02 19:41:02 -07:00
Scrates1andGitHub 679ff9161b fix(ibus): require a restorable engine for scoped injection (#623)
* fix(ibus): require a restorable engine for scoped injection

* fix(ibus): preserve registered XKB restore targets

Resolve GNOME XKB sources against registered IBus engine IDs so bare-XKB Wayland sessions keep scoped injection without fabricated restore names. Keep unknown states on the safe fallback path.

* test(ibus): cover GNOME restore target failures
2026-08-02 19:09:38 -07:00
27ecf880a8 fix(settings): restore custom shortcut Record/Set controls (#619)
* 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>
2026-07-30 08:49:25 -07:00
Jatin K MalikandGitHub 3e78a9bc4e feat(settings): move dictation controls into sidebar (#618)
Keep the persistent test and close actions accessible without an awkward
bottom action row, while using portable sidebar icons.
2026-07-29 01:34:57 -07:00
cc69c7ebda fix(languages): map English (India) to Whisper code en (#617)
* 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>
2026-07-28 21:31:02 -07:00
28bed4c1fd feat(languages): expand speech catalog with Hungarian and more (#616)
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>
2026-07-29 07:32:37 +05:30
Jatin K MalikandGitHub ca24264c85 fix(audio): stop Bluetooth mic probing from corrupting the heap (#599)
* 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%.
2026-07-29 02:49:55 +05:30
8322306aac feat(dictation): append trailing space after completed transcription (#608)
* 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>
2026-07-29 02:00:18 +05:30
7422bf927e feat: auto-capitalize sentences after punctuation (#554)
* 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>
2026-07-29 01:39:15 +05:30
8c76578200 fix(ui): flatten settings info notices (#615)
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>
2026-07-29 01:15:09 +05:30
Benjamin EiseleandGitHub 29db3a05a4 fix(ibus): keep engine teardown correct when parent destroy fails (#613)
* 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.
2026-07-29 00:36:24 +05:30
Benjamin EiseleandGitHub 161129cf3c feat(text-injection): use IBus on unbridged compositors when ibus-wayland runs (#614)
* feat(text-injection): use IBus on unbridged compositors when ibus-wayland runs

The `_IBUS_UNBRIDGED_COMPOSITORS` denylist assumes nothing sits between
the compositor and ibus-daemon. Since IBus 1.5.32 that is no longer a
safe assumption: IBus ships `ibus-wayland`, which implements
`zwp_input_method_v2` and relays commits to native Wayland clients
speaking text-input-v3. Every compositor on the denylist exposes
`zwp_input_method_manager_v2`, so with the bridge running they are
bridged, and skipping IBus falls back to a slower path unnecessarily.

Add a live `pgrep -x ibus-wayland` probe and let it override the
denylist. Scoped to the denylist branch only, so GNOME and the KDE
VirtualKeyboard path (#574) are untouched.

Behaviour is unchanged when the bridge is not running, and because the
check is a live probe rather than configuration it degrades back to the
denylist by itself if the bridge dies or was never started. Cost is one
pgrep per TextInjector construction, not per injection.

Verified on Hyprland 0.56.1 with IBus 1.5.34: dictation into Electron
apps (VSCodium, Electron 39/41) and kitty commits through IBus, 303
chars in ~200ms versus 16.8s for 415 chars via `ydotool type`.

Also adds VOCALINUX_FORCE_BACKEND (ibus|wtype|ydotool|auto) as discussed
in #607. Autodetection has to infer whether IBus commits actually reach
the focused app and cannot verify it -- commit_text() reports success
even when the text is dropped -- so an escape hatch is useful when the
inference is wrong, and it makes the two paths A/B-testable without
editing code. Unknown values fall back to auto with a warning rather
than silently pinning a backend. Related to #476 and #402; happy to drop
this from the PR if it should follow their design instead.

Two existing tests asserted the unbridged outcome without pinning the
probe -- one blanket-mocks subprocess.run to returncode=0, the other
shells out for real -- so both now state the "no bridge" precondition
explicitly rather than depending on the host.

Note for users: Chromium only opens a zwp_text_input_v3 seat when
launched with --enable-wayland-ime; without it the bridge runs and the
app still receives nothing.

Closes #607

* test(text-injection): cover bridge override and forced-backend branches

Codecov flagged the new branches in _check_dependencies as uncovered
(74% patch, below the 80% target).

Adds four tests to TestCheckDependencies:

- unbridged compositor + ibus-wayland running selects IBus
- VOCALINUX_FORCE_BACKEND=wtype pins wtype where IBus would be chosen
- VOCALINUX_FORCE_BACKEND=ydotool pins ydotool even with wtype present
- VOCALINUX_FORCE_BACKEND=ibus bypasses the reachability guards, with
  neither a daemon nor a bridge available

Every line added by this PR is now covered.
2026-07-29 00:36:14 +05:30
Jatin K MalikandGitHub 4fad897f14 fix(appimage): smoke-test fixes for tray, labels, and packaging (#602)
* fix(ui): AppImage smoke fixes for tray icons, labels, and identity

Replace placeholder mic tray SVGs with opaque branded icons, show
whisper.cpp instead of Whisper_cpp, set GLib/AppIndicator identity to
Vocalinux (not main.py), and give About dialog buttons enough padding
so labels are not clipped under Adwaita.

* fix(packaging): harden AppImage build for Wayland and tray deps

Pin the build interpreter, install onnxruntime into the bundle, set
desktop Exec=AppRun + StartupWMClass, prune typelibs to an allowlist
after linuxdeploy, and stop forcing GDK_BACKEND=x11 on Wayland
(VOCALINUX_FORCE_X11=1 remains as an escape hatch). CI AppImage jobs
also pull Ayatana AppIndicator and librsvg SVG loader packages.

* fix(packaging): bundle full stdlib from base_prefix in AppImage

When build.sh runs under a venv, sys.prefix only has site-packages, so
the AppImage was missing encodings and crashed on start. Copy the
interpreter and stdlib from sys.base_prefix, force pip
--ignore-installed into AppDir, set Gdk program class to Vocalinux, and
tone down the muted tray slash so small StatusNotifier icons stay slate.

* fix(tray): use absolute icon paths so stale hicolor icons lose

XFCE StatusNotifier resolves IconName through the system theme and can
ignore IconThemePath. A leftover ~/.local/share/icons placeholder
(vocalinux-microphone-off.svg filled #e74c3c) was winning over the
AppImage's bundled icons. Outside Flatpak, pass absolute SVG paths to
set_icon_full / new_with_path instead of bare theme names.
2026-07-27 19:57:07 -07:00
e7022f83dd feat(settings): searchable sidebar settings instead of seven tabs (#601)
* feat(settings): replace tab navigation with searchable sidebar and topic pages

- Sidebar (icon + label) with Gtk.Stack replaces the 7-tab notebook
- Live settings search (Ctrl+F): filters rows in place, per-page match
  counts, dims pages without hits, empty-results state
- Topic-based pages per GNOME HIG: Dictation, Speech Model, Audio,
  Performance, Application, Advanced
- Persistent bottom status strip: recognition state, shared mic level
  bar, and Test Dictation with inline transcription output
- GPU device selection moves out of the power-user gate onto the
  Performance page; Reset to Defaults moves into the Advanced page
- Consolidated model info card (info + warning + legend in one surface)
- Contextual custom-shortcut row, in-list auto-pause empty state,
  uniform control widths, aligned group descriptions
- Esc clears search, Ctrl+W closes

* test(settings): update source-inspection tests for sidebar redesign, add search coverage

* fix(settings): index unlocked advanced controls in search

Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>

* fix(settings): restore selected page after clearing search

Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
2026-07-27 14:31:39 -07:00
c59fe8b577 fix: skip unbridged IBus on KDE Plasma Wayland (#577)
* fix(injection): skip unbridged IBus on KDE Plasma Wayland

Do not auto-start XIM ibus-daemon on Wayland (circular viability check).
On KDE, require KWin VirtualKeyboard enabled before selecting IBus.

Fixes #574

* refactor(injection): tighten KDE VirtualKeyboard bridge check

Ponytail cleanup: shorter logs and fewer failure branches, same behavior.

* style: black-format ibus daemon Wayland test for CI

* fix(injection): parse KWin VirtualKeyboard gdbus variants strictly

Match <<true>>/<<false>> wrappers instead of bare substrings so ambiguous
gdbus output cannot flip the #574 IBus bridge decision.

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>
2026-07-26 23:06:26 -07:00
4c070ded46 feat(offload): auto-pause apps + idle keep-alive model unload (#592)
* feat(auto-pause): unload speech model while selected apps run

Add Settings toggle + process list so Vocalinux frees CPU/GPU/RAM while
configured games/apps are running, then reloads the model when they exit.
Closes the process-list half of #445 (window-focus pause is separate).

* test(auto-pause): isolate tray wiring to avoid stale GLib mocks

Move TrayIndicator auto-pause tests into test_suspend_handler so
test_auto_pause_monitor never imports tray_indicator under a collection-time
gi MagicMock. That import pinned GLib.SOURCE_REMOVE and broke resume tests
later in the suite (CI on #517).

* test(auto-pause): raise patch coverage for codecov target

Cover process snapshot I/O, GLib poll scheduling, config coercion,
unload edge cases, and tray fallback paths so codecov/patch clears 80%.

* feat(keepalive): idle model offload for GPU/battery saving

Add opt-in keep-alive that unloads the speech model after inactivity so
hybrid-GPU laptops can release VRAM/Vulkan. Next dictation lazy-reloads.
Shares unload_model with auto-pause (#445) using distinct reasons so
app-pause still blocks dictation while idle unload does not.

Fixes #591 (with #445 via shared offload infrastructure).

* test(keepalive): mock gi.repository.GLib via sys.modules

Match the auto-pause GLib test pattern so CI on Python 3.9/3.10
sees the late `from gi.repository import GLib` import.

* test(keepalive): drop post-fire source_remove assertion

After the GLib timeout callback runs, _timeout_id is already cleared,
so shutdown does not call source_remove.

* fix(settings): hide auto-pause empty-state label when apps are listed

The dialog-wide show_all() after _load_and_apply_settings() re-showed
the 'No apps listed yet' row that _refresh_auto_pause_list had hidden.
Mark the row no_show_all and manage visibility explicitly.

Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>

* feat(settings): move auto-pause and idle unload to dedicated Power tab

- New Power tab between Shortcuts and General so resource management
  features are easy to find; General returns to app-behavior toggles only
- Rename 'Model Keep-Alive (Battery)' to 'Unload When Idle' — the old
  name read as the opposite of what the feature does
- Clearer copy throughout: row titles, descriptions, tooltips,
  'From running…' becomes 'Choose Running App…', empty-state hint
- Gray out app-list and idle-timeout controls while their feature
  toggle is off

Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>

* fix(settings): show which notebook tab is selected

The tab CSS targeted :selected, but GTK3 marks the active notebook tab
with :checked, so no selected styling ever applied — all tabs rendered
identically. Use :checked and add an accent underline + bold label.

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>
2026-07-26 22:29:00 -07:00
Jatin K MalikandGitHub b945137d60 fix(vulkan): auto-select discrete GPU and add manual device selection (#590)
* fix(vulkan): auto-select discrete GPU and add manual device selection

On hybrid GPU systems (Intel iGPU + NVIDIA dGPU), whisper.cpp's Vulkan
backend always selected device 0 (the integrated GPU), resulting in
significantly slower performance.

Changes:
- Add detect_vulkan_devices() to enumerate all Vulkan GPUs and identify
  discrete vs integrated devices
- Set GGML_VULKAN_DEVICE env var before model init to select the correct
  GPU (auto-selects discrete GPU when available)
- Add whispercpp_gpu_device config option for manual device selection
- Add GPU device dropdown in Settings > Advanced for users with multiple
  Vulkan GPUs

Fixes #589

* test: add coverage for Vulkan GPU device selection

- Test detect_vulkan_devices() with single/multiple GPUs
- Test _prefer_discrete_vulkan_device() preference logic
- Test GPU device selection in recognition_manager
- Verify GGML_VULKAN_DEVICE env var is set correctly

* fix(speech_recognition): pass whispercpp GPU device to pywhispercpp Model context_params

The Vulkan device-selection logic from PR #590 set the GGML_VULKAN_DEVICE
environment variable, but pywhispercpp did not pick it up. Pass the selected
GPU device through the pywhispercpp Model context_params so whisper.cpp
actually initializes the requested Vulkan device.

Fixes #589

* fix(speech_recognition): fallback when pywhispercpp lacks context_params

Older pywhispercpp releases (< ~1.4.0) reject the context_params keyword
and raise TypeError. Detect that specific failure and retry without GPU
device selection so the engine still loads on those versions.

Fixes #589

* refactor(speech_recognition): inline context_params assignment

Simplify the GPU device selection path and remove the temporary dict.

* fix(vulkan): parse real vulkaninfo GPU0: headers

detect_vulkan_devices() only recognized fictional "GPU id = N" headers,
so real vulkaninfo --summary output (GPU0:/GPU1:) returned no devices and
hybrid NVIDIA systems fell through to CUDA. Also resolve preferred device
names by GPU id instead of list position.

Fixes #589

* refactor(whispercpp): drop ineffective GGML_VULKAN_DEVICE env var

pywhispercpp ignores GGML_VULKAN_DEVICE; device selection already goes
through context_params={"gpu_device": N}. Remove the misleading env write
so maintainers do not think the env var is the selection path.
2026-07-26 21:25:20 -07:00
Anay GarodiaandGitHub 2a0e11e077 fix: correct ydotool service setup guidance (#560) 2026-07-25 13:12:32 -07:00
Anay GarodiaGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
eb16a34328 feat(cli): add --version option 🤖🤖🤖 (#563)
* feat(cli): add version flag

* fix(cli): parse args before acquiring single-instance lock so --version works while app is running

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-25 13:04:52 -07:00
Jatin K MalikandGitHub 8c4f743e82 fix(ui): first-run dialog response without Gtk.Dialog.do_response (#580)
* fix(ui): use Gtk response signal for first-run dialog

Overriding do_response and calling Gtk.Dialog.do_response() breaks on
modern PyGObject with "Class GtkDialog doesn't implement response".
Connect to the response signal instead so dialog.run() still works.

Fixes #566

* test(ui): cover first-run dialog response handler

Exercise connect("response") wiring and _on_response mapping so
codecov/patch is green for the #566 Gtk.Dialog.do_response fix.

* test(ui): isolate first-run dialog response coverage

Load first_run_dialog under a unique module name with a real Gtk.Dialog
stand-in so _on_response and connect("response") run under full-suite
import pollution without clobbering other tests' gi mocks.

* test(ui): cover _on_response via SimpleNamespace

Call FirstRunDialog._on_response on a stand-in object instead of
constructing Gtk.Dialog; enough for codecov/patch on the handler body.

* test(ui): map first-run responses via SimpleNamespace

Exercise FirstRunDialog._on_response for YES/NO/LATER/unknown without
constructing Gtk.Dialog.

* test(ui): collapse first-run response cases into one table

One loop covers YES/NO/LATER/unknown instead of four copy-pasted tests.

* test(ui): restore isolated first-run dialog coverage load

SimpleNamespace calls against vocalinux.ui.first_run_dialog.FirstRunDialog
fail under the full suite when earlier tests leave the class as a MagicMock.
Reload the source under a unique module name with a real Gtk.Dialog stand-in
(as in 680c5ed) so _on_response and connect("response") stay green on CI.
2026-07-24 00:44:47 -07:00
Anay GarodiaandGitHub 513e4749e3 fix(xdotool): preserve input focus after injection (#564) 2026-07-23 21:44:49 -07:00
Anay GarodiaandGitHub fbcd28d577 fix(vosk): add missing Italian and English-India entries to medium/large model tables (#551)
Fixes #550.

Root cause: VOSK_MODEL_INFO["medium"]["languages"] and
VOSK_MODEL_INFO["large"]["languages"] in vosk_model_info.py were missing
entries for "it" (Italian) and "en-in" (English-India), even though both
languages are present in the "small" table and VOSK actually publishes
bigger models for them (vosk-model-it-0.22, vosk-model-en-in-0.5 at
alphacephei.com/vosk/models).

When a user selected Italian + the medium (or large) VOSK model,
_init_vosk() built vosk_model_map["medium"] = None (via .get() on a
missing key), and _get_vosk_model_path() then called
os.path.join(MODELS_DIR, None), crashing with:

    join() argument must be str, bytes, or os.PathLike object, not 'NoneType'

before any download was even attempted. This matches the exact debug log
in the issue and the settings-dialog download flow the reporter exercised.

Fix:
- Add the missing "it"/"en-in" entries to the medium/large tables so all
  10 supported languages now resolve consistently across all three sizes.
- Add a guard in _get_vosk_model_path() that raises a clear ValueError
  ("No VOSK '<size>' model available for language '<lang>'...") instead of
  crashing deep inside os.path.join, so any future data gap fails loudly
  and helpfully rather than with a cryptic TypeError.
2026-07-19 22:53:07 -07:00
Jatin K MalikandGitHub 3d65e5cea9 chore(release): prepare v0.14.2 (#537)
* chore(release): prepare v0.14.2

IBus stability patch on the 0.14 series: engine process launch after XDG
import (#534) and FocusIn gate for first dictation on GNOME Wayland (#533).

Keep 0.14 series feature list in README/UPDATE for patch releases; changelog
and AppStream note only the delta. Document that rule in RELEASE_PROCESS.

* fix(release): address 0.14.2 review findings

- Dates: v0.14.2 uses 2026-07-17 (not before 0.14.1)
- AUR: sha256sums=SKIP until tag; document updpkgsums in RELEASE_PROCESS
- Single version source: package __init__ re-exports version.py; User-Agent uses __version__
- Docs: drop redundant series recaps; shrink AppStream/changelog delta
- README ecosystem status: Stable v0.14.2 (not Beta)

* docs(release): include settings scroll fixes in 0.14.2 notes

Cover #538 and #541 (scrollable settings tabs / wheel polish) in README,
UPDATE, website changelog, AppStream, and version history after rebasing
onto main.
2026-07-17 18:42:53 -07:00
Jatin K MalikandGitHub 9e9f197a7b fix(ibus): wait for FocusIn before commit on scoped injection (#533)
* fix(ibus): wait for FocusIn before commit on scoped injection

Cold first activation of the vocalinux engine on GNOME Wayland could
commit before mutter bound a client context, so logs reported success
while the first dictation of a session was dropped.

Fixes #523

* refactor(ibus): clear focus event directly in do_destroy

Drop the clear_focus callback on the destroy helper; one local branch is enough.

* test(ibus): cover FocusIn gate helpers for patch coverage

Extract PING/focus-wait helpers and exercise them so codecov/patch
clears the 80% target on the #523 fix.
2026-07-16 22:38:59 -07:00
Jatin K MalikandGitHub 1e81b330c0 fix(ibus): restore engine process launch after XDG path import (#534)
* fix(ibus): restore engine process launch after XDG path import

PR #484 switched ibus_engine to a relative import of xdg_data_home for
Flatpak-safe paths. start_engine_process and the IBus component exec still
run this file by path (python /path/to/ibus_engine.py), which has no
package context, so the process died immediately with ImportError and
Vocalinux fell back to ydotool (clipboard paste).

Accept absolute/inline import fallbacks when loaded as a script, log child
stderr from engine.stderr.log on startup failure, and add a path-based
--xml smoke test so this does not regress.

* test(ibus): cover start_engine_process failure diagnostics

Raise patch coverage for the engine.stderr.log failure paths so
codecov/patch meets the 80% target on #534.

* chore: re-trigger CI after rebase onto main
2026-07-16 22:38:32 -07:00
Jatin K MalikandGitHub 853c1504b2 chore(release): prepare v0.14.1-beta (#525)
Patch release for Flatpak/AUR packaging and post-0.14.0 fixes:
installer wrappers, ydotool dual API and Flatpak socket probe, model
download timeouts, layout-aware hotkeys, and related docs/web bumps.
2026-07-16 17:51:45 -07:00
Jatin K MalikandGitHub 3deb523efd feat(flatpak): add Flatpak packaging for universal distribution (#484)
* feat(flatpak): add Flatpak packaging for universal distribution

Adds a Flathub-ready Flatpak for the whisper.cpp engine, plus the minimal
runtime adaptations Flatpak actually needs — deliberately scoped down from the
earlier attempts (#416, #428), which bundled unrelated whisper.cpp GPU, audio,
and clipboard changes.

Packaging (packaging/flatpak/):
- app manifest, AppStream metainfo, and desktop entry (com.vocalinux.Vocalinux)
- generated offline Python dependency modules (flatpak-pip-generator)
- bundled shared modules: PortAudio, libayatana-appindicator, xdotool, xsel

Runtime changes (Flatpak-only, behaviour unchanged outside a sandbox):
- utils/paths.py: honour XDG_CONFIG_HOME/XDG_DATA_HOME so config, models and
  runtime data live under ~/.var/app/<app-id> in the sandbox
- autostart uses `flatpak run <app-id>` (the host launcher is always on PATH)
- text injection and keyboard backend detect a Wayland host reached via XWayland
  (no Wayland socket granted) and route through xdotool / pynput-on-X11
- tray indicator references the exported `<app-id>-*` host icon theme names

Also: a CI workflow building x86_64 + aarch64 bundles, and README / packaging
README / distro-compatibility doc updates.

Verified with a full flatpak-builder build + bundle install on aarch64:
pywhispercpp loads its native libraries without LD_LIBRARY_PATH, XDG paths
redirect correctly, bundled tools are present, and the entrypoint runs.
Manifest and builddir lint are clean and AppStream metadata validates.

Closes #167

* chore(flatpak): sync metainfo version to 0.14.0-beta

Also ignore flatpak-build-dir/ so local builder output stays untracked.

* refactor(flatpak): trim docs and comments after ponytail review

Drop redundant packaging README command blocks, shrink path/tray/autostart
comments, and stop ignoring a non-canonical build-dir name.

* fix(flatpak): close readiness gaps for v0.14.0-beta

Route log files through XDG data dirs so Flatpak keeps them under
~/.var/app, refresh AppStream screenshots to vocalinux.com, and use
the SPDX GPL-3.0-only license id.

* fix(flatpak): enable global hotkeys via evdev and --device=input

pynput over XWayland only receives keys for the focused X client. Prefer
evdev inside Flatpak and grant /dev/input so shortcuts work app-wide.

* fix(flatpak): inject text with ydotool/uinput on Wayland

xdotool only reaches XWayland clients, so typing appeared to succeed but
never hit native apps. Ship ydotool, grant --device=all for uinput, start
ydotoold on demand, and type via uinput inside the Flatpak.

* fix(flatpak): paste via wl-copy + Ctrl+V instead of typing

ydotool type injects character-by-character. Ship wl-clipboard, grant
--socket=wayland, and prefer clipboard paste for instant insertion.

* test(text-injection): expect ydotool default key-delay of 2

Matches the shipped VOCALINUX_YDOTOOL_KEY_DELAY default used when
clipboard paste falls back to character typing.
2026-07-16 00:03:30 -07:00
Jatin K MalikandGitHub 0a7acaea2f fix(hotkeys): layout-aware combo keys for non-US layouts (#513) (#514)
* fix(hotkeys): resolve combo letters via active XKB layout

Settings capture stores character tokens from GDK keyvals (e.g. alt+a),
but the Wayland/evdev path mapped letters to fixed US KEY_* codes. On
AZERTY that made Alt+A listen for the key labeled Q. Resolve letter
tokens through libxkbcommon for the active layout so recording and
listening agree.

Fixes #513

* refactor(hotkeys): slim layout-aware key resolution

Drop unused resolve helper, dual knobs, setxkbmap/localectl probes, and
redundant tests. Keep gsettings + libxkbcommon map and the AZERTY combo path.

* test(hotkeys): drop redundant AZERTY resolve-only case

Keep fire/no-fire combo coverage and the XKB fr map check.

* test(hotkeys): cover layout map detection and error paths

Raise patch coverage for layout_key_map / evdev_code_for_key so codecov
patch target (80%) is met.
2026-07-14 19:02:26 -07:00
Jatin K MalikandGitHub 1441be0512 fix(text-injection): treat XIM none as unset (#512) 2026-07-14 18:50:28 -07:00
Jatin K MalikandGitHub f456160c45 fix(ui): keep preset and custom shortcut selection exclusive (#509)
Selecting a preset after recording a custom combo left the custom entry
filled, so both looked active. Sync the dropdown to "Custom Shortcut"
when a non-preset is set, and clear the custom field when a preset wins.
2026-07-13 18:53:00 -07:00
Jatin K MalikandGitHub 872a745bc4 fix(text-injection): restore GNOME Wayland IBus with bare xkb engine (#506)
#502 only exempted KDE from the #478 real-engine gate. GNOME/Mutter has
the same scoped-activation path, so stock sessions with only an xkb:*
layout fell through to wtype, which Mutter rejects, then clipboard-only.

Allow any Wayland session without an explicit non-IBus IM to try IBus
when ibus-daemon is running; unbridged compositors still hit the
existing denylist.

Fixes #504
2026-07-13 15:12:30 -07:00
Jatin K MalikandGitHub 85927be4b4 feat(asr): FunASR/SenseVoice support for the Remote API engine (#468)
* feat(asr): add chat audio remote benchmarking

* style(asr): apply black formatting

* test(main): include remote API model kwarg

* test(asr): add coverage for chat-completions error handling and parsing

Add 10 new tests to cover uncovered lines in PR #468:
- ConnectionError handler in _try_chat_completions_audio_api (lines 1601-1602)
- _parse_chat_completion_transcription edge cases: None, list content,
  empty/whitespace strings, JSON without text field, plain text, and
  single-line language metadata (lines 1610, 1613-1621, 1625, 1629-1640)

Coverage for recognition_manager.py improved from 85% to 86%.

* feat(asr): support FunASR remote responses

* refactor(asr): scope remote FunASR support to the #469 path

Drop chat-completions/Qwen client, evaluation notes, and the remote ASR
benchmark harness. Keep model field, tolerant OpenAI response parsing,
SenseVoice label stripping, FunASR docs, and tests.
2026-07-13 15:03:19 -07:00
b3605bf3c6 feat(shortcuts): configurable modifier+key hotkeys (e.g. Alt+R) (#493)
* feat(shortcuts): configurable modifier+key hotkeys (e.g. Alt+R)

Generalize the shortcut system beyond single-modifier double-tap/hold to
support modifier+key combos (alt+r, ctrl+alt+r, super+space) in both toggle
(press once) and push-to-talk (hold) modes.

- base.py: ShortcutSpec + parse_shortcut_spec/is_valid_shortcut/format label;
  parse_shortcut kept for back-compat (bare modifiers still rejected).
- evdev + pynput backends: combo detection with global (cross-device) modifier
  tracking so split keyboards work; legacy gestures unchanged.
- keyboard_shortcuts: validate via is_valid_shortcut instead of a fixed allowlist.
- settings_dialog: additive Custom Shortcut row (typed entry + live Record capture).
- tests: 34 new tests (parser, back-compat, evdev + pynput combo, split keyboard).

* fix(shortcuts): apply saved shortcut (not stale preset) on mode change

When a custom shortcut (e.g. alt+r) is active, the preset combo still points at
a default/previous preset. _on_shortcut_mode_changed read the combo, so toggling
Toggle<->Push-to-Talk restarted the live listener on the wrong shortcut while
config still said alt+r. Read the saved shortcut from config instead.

Addresses Codex P2 review on PR #493.

* style: apply black formatting to configurable-hotkeys files

* test: cover _gdk_keyname_to_token shortcut-capture mapping

* fix(shortcuts): reset combo state after dropped/lost evdev events

SYN_DROPPED and device-disconnect recovery cleared key_pressed_devices but not
the combo state. A dropped modifier *release* left _combo_pressed thinking the
modifier was held, so the main key alone could falsely toggle/start dictation,
and a push-to-talk hold could stay stuck. Add _reset_combo_state() (clears the
pressed set and ends any live hold) and call it from both recovery paths.
Addresses maintainer review on PR #493.

* fix(settings): combo-aware toggle label (single press, not double-tap)

_update_shortcut_ui_for_mode always said 'Double-tap the key' for toggle mode,
but a modifier+key combo (e.g. alt+r) toggles on a single press. Read the saved
shortcut and use get_shortcut_display_name so a combo shows 'Press Alt+R'.
Addresses maintainer review on PR #493.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-07-13 14:43:36 -07:00
Ben WilliamsandGitHub a2a5c569c5 fix(text-injection): wait for modifiers to release before Wayland injection (#494)
* fix(text-injection): wait for shortcut modifiers to release before Wayland inject

Injection emits real key events, so a held modifier from a toggle/PTT shortcut
(e.g. Alt from Alt+R) turns the simulated Ctrl+V paste into Ctrl+Alt+V and
nothing pastes — while the clipboard still shows the text. Because transcription
can finish in tens of ms, the user is often still holding the key, causing
intermittent paste failures.

Poll evdev for held modifier keycodes and defer injection until they clear
(bounded by VOCALINUX_INJECT_MODIFIER_WAIT, default 1.0s, 0 disables). Returns
immediately when nothing is held, so no added latency in the common case.

* style: black-format injection modifier-wait test

* test: cover _held_modifier_keycodes evdev scanning (patch coverage)

* test: cover error paths in _held_modifier_keycodes

* fix(text-injection): treat non-finite modifier-wait env as default

float('inf') parses cleanly and is > 0, so it slipped past the guard and made
the wait deadline infinite -- a held modifier could block injection forever
(nan was nonsensical too). Sanitize VOCALINUX_INJECT_MODIFIER_WAIT via a helper
that falls back to the 1.0s default for non-finite values. Regression tests
added, including that 'inf' still terminates.

Addresses maintainer review on PR #494.
2026-07-13 14:40:04 -07:00
Jatin K MalikandGitHub a9a6ae3c51 fix(text-injection): restore KDE Wayland IBus path (#502)
* fix(text-injection): restore bridged Wayland IBus path

* fix(text-injection): keep GNOME xkb fallback
2026-07-10 17:27:41 -07:00
4e3637dfb5 Fix: IBus engine restore uses wrong fallback on GNOME/Wayland (#497) (#500)
* Fix: IBus engine restore uses wrong fallback on GNOME/Wayland (#497)

On GNOME/Wayland, when IBus has no global engine (common when Mutter
manages input sources), 'ibus engine' can return a misleading
'xkb:us::eng' default that doesn't match the user's actual keyboard
layout. When Vocalinux restores this wrong value after text injection,
Electron apps switch to US layout.

Root cause: get_current_engine() trusted the output of 'ibus engine'
without checking whether IBus actually had a global engine set, and
had no fallback to discover the real keyboard layout.

Fix:
- Add get_current_engine_gnome_fallback() that reads the actual
  keyboard layout from org.gnome.desktop.input-sources via gsettings
- Modify get_current_engine() to detect 'No global engine' in stderr
  and to detect the suspicious 'xkb:us::eng' default on GNOME/Wayland,
  falling back to gsettings in both cases
- Modify stop() to log a message when skipping engine restoration
  because no valid engine was captured (None means don't restore)
- Modify inject_text() to log when skipping restoration after
  injection for the same reason
- Backward compatible: if gsettings is unavailable, falls back to
  current behavior (returning None or the IBus-reported value)

Tests: 12 new tests covering GNOME fallback, 'No global engine'
detection, suspicious default detection, gsettings parsing, and
edge cases.

* ponytail: simplify gsettings uint32 parsing

gsettings prints 'uint32 N' or just 'N' — split()[-1] handles both,
no need for separate branches

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
2026-07-11 05:42:00 +05:30
3303d075a6 Fix: Audio device index mismatch causing crash on recording start (#498) (#499)
* fix(audio): resolve device by name to prevent index mismatch crash (#498)

Audio device indices can shift between sessions when USB devices are
replugged or virtual devices are added/removed. A saved device_index=24
(BRIO webcam) could later point to 'speech-dispatcher-dummy', causing
PortAudio to attempt recording from a virtual device and triggering
malloc corruption / SIGABRT.

Fix has three parts:

1. Store and match by device name, not just index
   - Config already saves device_name alongside device_index
   - At recording time, _resolve_device_by_name() enumerates current
     devices and finds the one matching the saved name
   - Falls back to stored index, then system default (with warning log)
   - main.py now passes audio_device_name to SpeechRecognitionManager

2. Filter virtual devices from input device enumeration
   - _is_virtual_device() detects speech-dispatcher, dummy, null sink,
     monitor, and ALSA output devices
   - get_audio_input_devices() skips virtual devices
   - Settings dialog dropdown only shows real input devices
   - Debug logging in _record_audio also skips virtual devices

3. Validate resolved device has input channels (from flatpak branch)
   - Extra safety check: after resolving, verify maxInputChannels > 0
   - Falls back to system default if resolved device is invalid
   - Brings the validation from feature/flatpak-packaging to main

Closes #498

* style: apply black formatting to recognition_manager.py

* test: add audio_device_name kwarg to mock assertion

* test: add coverage for _is_virtual_device, _resolve_device_by_name, device name management

* test: fix StopIteration in fallback test by using return_value for mock

* ponytail: remove redundant validation and simplify _resolve_device_by_name

- Remove ~20 lines of dead validation in _record_audio() — _resolve_device_by_name
  and _resolve_valid_input_device already check maxInputChannels and virtual devices
- Simplify _resolve_device_by_name to just find index by name, then delegate all
  validation to _resolve_valid_input_device (which already does channel/virtual checks)
- Update tests to match the simplified delegation pattern

* style(speech): format device fallback warning

* test(speech): cover audio device fallback edges

* test(speech): cover reconnection device fallback

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
2026-07-09 16:37:45 -07:00
Jatin K MalikandGitHub 8f32f01f0e fix(installer): include xsel for Wayland clipboard fallback (#496) 2026-07-02 10:33:28 -07:00
Jatin K MalikandGitHub 6bb6d8802e fix(ibus): require a real IM engine on Wayland (#478) (#491)
* fix(ibus): require a real IM engine on Wayland (#478)

On Wayland a bare xkb:* IBus engine is a layout passthrough that never
delivers commit_text() to applications, so selecting the IBus backend
there silently no-ops (niri/sway, and GNOME/Mutter via the env-var
branch). Require a real IM engine before treating IBus as active on
Wayland, regardless of how it was detected; X11/XWayland (XIM) is
unchanged. Complements #486's compositor denylist.

* test(ibus): bind #478 patches to the module under test

Patch get_current_engine/is_ibus_daemon_running via patch.object on a
held ibus_engine reference and call is_ibus_active_input_method through
it, so the patched functions and the function under test always share
one module object. Fixes flakiness in the full CI suite where other test
files re-import ibus_engine and the dotted-string patch bound a
different module object.
2026-06-30 20:19:01 -07:00
Jatin K MalikandGitHub 943d2b5cc1 fix(ibus): don't run setxkbmap on Wayland (#474) (#490)
On Wayland, setxkbmap only reaches the XWayland X11 server, so
restore_xkb_layout() rewrote the XWayland layout (flipping it to us for
XWayland/Electron apps) while native Wayland apps stayed correct. Make
restore_xkb_layout() a no-op on Wayland and stop defaulting the captured
layout to 'us' so a never-captured layout is never re-applied.
2026-06-30 20:18:58 -07:00
Jatin K MalikandGitHub 34eb2f7ecf feat(text-injection): speed up ydotool injection with explicit --key-delay (#488)
Pass --key-delay 8 to ydotool type instead of relying on the default
(12-20ms). This makes text injection ~3x faster while keeping key-delay
above zero to avoid the Shift-leak bug where capital letters corrupt
following characters.

The delay is configurable via VOCALINUX_YDOTOOL_KEY_DELAY env var.

Note: --key-hold flag from the original issue does not exist in standard
ydotool (verified against official man page and v1.0.4 source).

Fixes #482
2026-06-30 19:55:55 -07:00
03b0efb51d fix(text_injection): use wtype on compositors that don't bridge IBus to native apps (#486)
* fix(text_injection): use wtype on compositors that don't bridge IBus to native apps

On Wayland compositors that don't bridge IBus to native clients (COSMIC,
Sway, Hyprland, Wayfire, river, niri, ...), the IBus engine's commit_text()
reaches only XWayland and GTK/Qt apps that load the IBus IM module. Native
apps such as cosmic-term receive nothing, yet inject_text() reports success,
so dictation silently does nothing.

- Add a compositor denylist (_wayland_compositor_bridges_ibus) so IBus is
  skipped on those compositors and the virtual-keyboard path (wtype/ydotool)
  is used instead. A denylist (rather than an allowlist) keeps the existing
  IBus-preferred behaviour on unrecognised desktops. Complements the recent
  KDE Plasma Wayland handling, which covers the mirror-image case.
- Replace the unreliable `ydotool type ""` probe (exits 0 even with no daemon)
  with _is_ydotoold_running(), so a daemonless ydotool install correctly falls
  back to wtype instead of selecting a no-op ydotool.
- Add unit tests for the compositor gate, the ydotoold check, and that an
  active-IBus COSMIC session selects wtype.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(text_injection): cover ydotoold socket-env and ydotool direct-mode paths

Add tests for the two new branches codecov/patch flagged as uncovered:
the explicit YDOTOOL_SOCKET path in _is_ydotoold_running(), and the
ydotool direct-mode fallback when no daemon and no wtype are available.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 19:24:55 -07:00
Farès SIONIandGitHub 14972672c5 fix(wayland): reliable clipboard copy and ydotool injection (#480)
Two independent text-injection bugs on Wayland:

1. Clipboard copy silently failed. wl-copy/xclip/xsel fork a background
   process that keeps owning the selection; capturing its stderr with
   subprocess.PIPE makes run() block on the surviving child until the
   clipboard is next overwritten, so the call always hits its timeout and
   _copy_to_clipboard reports failure even though the copy succeeded.
   Redirect stdout/stderr to DEVNULL so run() only waits for the
   short-lived foreground process.

2. ydotool scrambled text on non-US keyboard layouts. `ydotool type`
   emits positional evdev keycodes that get re-interpreted through the
   active layout (assumed US QWERTY), so dictated text came out garbled on
   AZERTY/QWERTZ/Dvorak (e.g. "message" -> ",essqge") and non-ASCII was
   dropped. Always inject via clipboard paste (Ctrl+V) for ydotool, which
   is layout-independent and Unicode-safe.

Adds regression tests for both.

Claude-Session: https://claude.ai/code/session_01EhyratrEE4vDWhriirUB9k
2026-06-30 19:24:51 -07:00
Jatin K MalikandGitHub c64b7270b3 fix(evdev): rescan hotplugged keyboard devices (#467)
* fix(evdev): rescan hotplugged keyboard devices

* test(evdev): cover hotplug monitor edge cases
2026-06-11 22:47:18 -07:00
Jatin K MalikandGitHub e0ac5e409c feat(whisper): add guided whisper.cpp model variants (#465)
* feat(whisper): refine whisper.cpp model selection

* refactor(whisper): exclude diarization model from picker

* docs(whisper): document guided model variants

* fix(ui): cap settings dialog height at 600px to prevent excessive height on high-res displays
2026-06-11 22:22:43 -07:00
Jatin K MalikandGitHub 9c5065f452 fix(input): clarify KDE Wayland IBus setup (#466) 2026-06-10 16:08:11 -07:00
Jatin K MalikandGitHub a73eec4a0a fix(dictation): preserve spacing between silence segments (#464) 2026-06-10 15:44:27 -07:00
Jatin K MalikandGitHub 49858f14e3 fix(ibus): capture current engine during prepare_engine for scoped activation (#458)
* fix(ibus): capture current engine during prepare_engine for scoped activation

Two bugs were causing IBus injection to fail and fall back to ydotool:

1. get_current_engine() returned empty string (falsy) when 'ibus engine'
   output was empty, instead of None. This caused inject_text() to think
   no engine was available.

2. prepare_engine() didn't capture the current IBus engine, so when
   inject_text() tried to get it at runtime, it would fail on systems
   where 'ibus engine' returns empty output.

Fix: get_current_engine() now returns None for empty output. prepare_engine()
captures the current engine before starting the Vocalinux process. inject_text()
uses the captured _previous_engine as fallback when get_current_engine() fails.

Fixes the regression from PR #457 where IBus injection would fail with:
  WARNING  Could not determine current IBus engine
  ERROR    IBus engine instance is not active and the previous engine
           could not be captured safely

* fix(ibus): always activate engine for injection on Wayland

On Wayland, 'ibus engine' returns empty output, so get_current_engine()
returns None. The previous code would skip activation when it couldn't
determine the current engine, causing injection to fail and fall back
to ydotool.

Now we always activate the Vocalinux engine when it's not active,
regardless of whether we can capture the previous engine. If we couldn't
capture it, we simply don't restore anything after injection.

This fixes the regression where IBus injection would fail with:
  WARNING  Could not determine current IBus engine
  ERROR    IBus engine instance is not active and the previous engine
           could not be captured safely

Fixes #458

* test(ibus): add missing mocks for is_engine_active and get_current_engine

Existing tests didn't mock is_engine_active() and get_current_engine(),
which are now called at the start of inject_text() for scoped activation.
This caused CI failures on Python 3.9-3.13 where the unmocked calls
would try to run 'ibus engine' and get unexpected results.
2026-06-06 20:41:15 -07:00