cd388b2e935710e32e6714cb573ed589df150d5c
287
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cd388b2e93 |
fix(evdev): strip SW_RFKILL_ALL from uinput clone capabilities (#947)
The evdev uinput clone copied EV_SW capabilities verbatim. "ThinkPad Extra Buttons" (thinkpad_acpi) declares SW_RFKILL_ALL, and a fresh uinput device reports every switch as 0. The kernel rfkill-input handler reads a 0 killswitch as radios-off, so every clone creation soft-blocked all radios at startup and again after every resume. Strip SW_RFKILL_ALL from the clone capability list, dropping EV_SW entirely when it was the only switch. Other switches stay declared so real switch events still forward; a forwarded SW_RFKILL_ALL write is dropped by the kernel for the undeclared code. Fixes #946 |
||
|
|
b0fefd6ec1 |
fix(evdev): leave held keyboards ungrabbed at startup (#941)
Solid fix, this was leaving keys stuck on the desktop whenever someone was typing while the app started. |
||
|
|
05ae041220 |
test(installer): cover the remote curl-to-bash install path end to end (#898)
Nice, this locks in the remote install path and keeps the model size check working on Python 3.13. |
||
|
|
4beb58c585 |
fix(packaging): build bundled pywhispercpp without -march=native (#939)
Good catch, this is what was crashing the AppImage and Flatpak on older CPUs. |
||
|
|
9d667dba3f |
feat(overlay): live mic orb with audio-reactive pulse and ripple (#940)
Looks great, the live level feedback makes the overlay feel a lot more responsive. |
||
|
|
e9598d64ce |
fix(installer): keep piped script on stdin and prompt via /dev/tty (#935)
* fix(installer): keep piped script on stdin and prompt via /dev/tty `curl | bash` left bash reading the installer itself from stdin, and the detection block's `exec < /dev/tty` (from #315) closed that pipe about 300 lines into a 3000-line script. curl died with error 23 writing to the closed pipe, and bash read the rest of the installer from the terminal as commands instead of finishing the script. stdin now stays on the pipe and a reachable terminal is recorded in TERMINAL_PROMPTS. Every interactive prompt goes through read_prompt, which reads /dev/tty explicitly only for that read, so user input still works in a terminal while the script body keeps flowing through the pipe. The interactive-mode gate now checks TERMINAL_PROMPTS rather than `-t 0`, which also lets `--interactive` work under a pipe. Regression coverage: the remote-install gate's second run now pipes the bootstrap in under a `script` pty (the literal `cat | bash` shape a curl user takes), and new tests run install.sh in a pty with the script on a pipe to assert the mode and engine prompts still answer on the terminal. Fixes #933 * fix(installer): drain the piped bootstrap before the handoff exec A curl|bash bootstrap leaves its unread tail on stdin; the tagged installer reads its commands from a file, so nothing drained the pipe and a still- streaming curl reported error 23 when the installer exited. Drain stdin to EOF before exec (Greptile P2 on #935). Gate: drip-feed the piped run so the producer is mid-write at the handoff like real curl, and run it under pipefail so a producer killed early fails the run. Tests: pass --tag so a slow release lookup cannot eat the window, wait for the engine prompt before Ctrl+C, and quote the installer path. * test(installer): propagate a killed producer's status in the piped gate A cat dying mid-drip (SIGPIPE when the consumer abandons the pipe) was swallowed by the loop and the final rm, so drip returned success and pipefail could not see the download failure the arm exists to catch. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
908fbb6072 |
fix(evdev): monitor remapper virtual keyboards ungrabbed (#932)
* fix(evdev): monitor remapper virtual keyboards ungrabbed On Wayland the device hub grabs every discovered keyboard and clones it via uinput so the dictation shortcut can be suppressed. With a remapper like keyd running, the hub grabbed its virtual keyboard too, and the remapper grabbed the clone in turn (keyd's default [ids] * matches it): forwarded keys went to the remapper instead of the compositor, so users could not type at all, and each forwarded event looped back through the chain. Detect virtual keyboards by their sysfs device path (uinput-spawned devices have no physical parent and live under /devices/virtual/input), falling back to known remapper names when sysfs is unreadable, and monitor them ungrabbed. Their events still reach the listener, so shortcuts bound to them keep working; only suppression is lost, which a grab could never provide anyway since the remapper re-emits on a device we cannot hold. keyd reports BUS_USB for its virtual devices, so the bus type cannot discriminate; the sysfs path can. Fixes #930 * test(evdev): annotate new #930 test mocks Satisfies the repo rule that every function signature carries type hints, including test fixtures. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
6595b73c55 |
fix(ui): unbreak Initial Prompt TextView and dedupe save-dialog .txt (#927)
Two small UI fixes found during the v0.18.1 smoke test. * Settings > Advanced > Initial Prompt never accepted typed input: the wrapping GtkListBoxRow grabs keyboard focus on button press and steals it back from the GtkTextView. Mark the row non-focusable. * Transcript and log save dialogs could return a doubled .txt.txt name; new paths.collapse_repeated_extension helper normalizes it, with a replace-confirmation when the collapsed path already exists. |
||
|
|
8ce85f1e9e |
fix(whispercpp): reuse an on-disk same-size weight at startup (#923)
* fix(whispercpp): reuse an on-disk same-size weight at startup The Settings picker already lets a downloaded same-size model stand in when the language-derived sibling is absent, but the startup path never applied it: a headless config with language=en-us and model_size=medium demanded ggml-medium.en.bin while ggml-medium.bin sat in the models directory and loads fine. Move the stand-in into whispercpp_model_info.on_disk_stand_in so both paths share it, and give resolve_whispercpp_variant a prefer_on_disk flag that only language-derived results use: pinned variants and stored specializations still load exactly what they name, the .en preference still wins when that file is present, and checksum verification is untouched. get_model_size_for_engine opts in so startup and headless launches match the Settings semantics. Fixes #916 * fix(whispercpp): keep is_model_downloaded free of makedirs get_model_path creates the model directory for callers that are about to write a file, but is_model_downloaded is a read-only probe: on a fresh profile its makedirs ran inside tests that stub os.makedirs for the config directory, breaking them, and a bare existence check should not mutate the filesystem anyway. Resolve the file path without creating directories in the probe; get_model_path keeps creating for writers. Refs #916 * test(whispercpp): annotate the stand-in fixture's return type AGENTS.md requires type hints on every signature; _downloaded returns the context manager from patch.object. Refs #916 --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
295386f08b |
fix(text-injection): end option parsing before typed text (#922)
* fix(text-injection): end option parsing before typed text A 20-char xdotool chunk, or a 200-char wtype/ydotool chunk, that starts with '-' (a word hyphenated across the chunk boundary) is parsed as a flag: 'xdotool type --clearmodifiers -stickation' exits 1 with 'type: unrecognized option' and the rest of the dictation is dropped. Pass '--' before the text argument in the xdotool, wtype, and ydotool type invocations so the remaining argument is always typed literally. Fixes #921 * test(portal-fallback): expect the wtype option terminator The partial-type fallback assertion predates the -- terminator and still expected the bare ['wtype', 'llo'] argv. Refs #921 --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
d021e64813 |
fix(dictation-pad): keep pad window out of the compositor ghost state (#912)
* fix(dictation-pad): keep pad window out of the compositor ghost state On GNOME Wayland an idle, fully covered toplevel gets shelved by the compositor: it stays in the window list but renders nothing and takes no input. Keep the pad floating above other windows and pinned to all workspaces so it cannot be shelved, track iconify/obscure state events, deiconify on show, and repaint when visibility returns. Fixes #896 * test(dictation-pad): scope Window return_value patch in shelving test * fix(dictation-pad): detect Wayland shelving and recreate dead surface set_keep_above is a stub in GTK3's Wayland backend, so a covered idle pad still gets shelved by the compositor on GNOME/Wayland: it stays in the window list but draws nothing and takes no input, and present() cannot revive it. Add a frame-callback watchdog: each tick callback stamps the clock and a GLib timeout flags a mapped window whose frames went cold as shelved. A shelved pad is then recreated (hide + show_all) in show_pad, the same path the user's manual Close + tray reopen takes. Keep keep_above/stick for X11 where they are honored, plus the iconify and visibility-event handling from the previous commit. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
06f475cf95 |
fix(evdev): require a real keyboard key in /proc keyboard discovery (#918)
* fix(evdev): require a real keyboard key in /proc keyboard discovery Any nonzero B: KEY bit qualified a device as a keyboard, so power/sleep buttons, the ACPI video bus, vendor hotkey blocks and pointer-button interfaces were all grabbed and given (vocalinux) uinput clones, leaving duplicate logical devices that confuse compositor input settings. Require a modifier or KEY_A in the KEY bitmap — the same rule the evdev fallback path already applies — and update the test fixtures to a real keyboard bitmap so they exercise the intended gate. Fixes #915 * fix(evdev): read /proc bitmaps at the kernel's word width Capability bitmap words are unsigned longs — 32 bits on 32-bit kernels (armv7l, i686), not 64. Parse the whole bitmap through _bitmap_to_int() sized by platform.machine() so high key codes like KEY_RIGHTMETA land correctly on both widths. Give pointer-test mocks real keyboard keys so the pointer check — not the keyboard-key gate — drops them. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> Co-authored-by: Jatin K Malik <jk.malik@yahoo.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
ecd4ae8388 |
fix(evdev): skip multitouch-only touchpads in keyboard discovery (#917)
* fix(evdev): skip multitouch-only touchpads in keyboard discovery Devices reporting only ABS_MT_POSITION_X/Y (no single-touch ABS_X/Y) evaded the pointer-axis check, so multitouch touchpads like the Goodix GXTP5100 were grabbed and cloned — breaking pointer input while running. Fixes #914 * fix(evdev): read /proc bitmaps at the kernel's word width Capability bitmap words are unsigned longs — 32 bits on 32-bit kernels (armv7l, i686), not 64. Parse the whole bitmap through _bitmap_to_int() sized by platform.machine() so MT axis bits and any future high bits land correctly on both widths. Give the fallback MT-mock a real key so the MT-axes check is the only thing that can drop it. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> Co-authored-by: Jatin K Malik <jk.malik@yahoo.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
622d0e054d |
fix(text-injection): detect KWin 6 VirtualKeyboard property (#919)
* fix(text-injection): detect KWin 6 VirtualKeyboard property On KDE Plasma 6 the org.kde.kwin.VirtualKeyboard 'enabled' property no longer exists; the setting is reported as 'available'. Query 'available' first, keep 'enabled' as the Plasma 5 fallback, and accept both the real (<true>,) gdbus output and the doubly wrapped (<<true>>,) form. With the bridge check working, KDE sessions with Virtual Keyboard set to "IBus Wayland" can safely use scoped IBus injection again: the leftover-daemon trap of #752 is exactly what a confirmed VK bridge disambiguates. KDE without a confirmed VK stays excluded. KWin's RemoteDesktop portal scrambles letter case, so in auto mode on KDE sessions an installed ydotool is preferred over the portal -- the ordering used before the portal backend existed. Fixes #911 * fix(text-injection): keep KDE portal fallback ahead of daemonless ydotool On KDE, when ydotool is installed but cannot run (no daemon, no /dev/uinput access) and wtype is absent, the daemonless-ydotool branch fired before the portal fallback -- selecting a backend that fails at injection time while a working portal sat unused. The portal fallback now precedes the daemonless branch; working ydotool still outranks the portal on KDE. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
9eba80b53d |
fix(packaging): declare python3-pysocks in deb and rpm deps (#920)
* fix(packaging): declare python3-pysocks in deb and rpm deps #909 added PySocks as a runtime dependency for SOCKS5 proxy support but only covered the AUR PKGBUILD — the nfpm deb and rpm packages never declared python3-pysocks, so test_every_project_dependency_is_covered_in_both_formats fails on main and on every PR's merge check. Add it to both depends lists and the test's DEB/RPM_PROVIDES maps. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(packaging): deb name is python3-socks Debian/Ubuntu package PySocks as python3-socks (1.7.1+dfsg); the rpm name stays python3-pysocks — Fedora's python3-socks is a different library (python-socks). Verified in debian:12, ubuntu:24.04 and fedora:42 containers. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Jatin K Malik <jk.malik@yahoo.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
329a7d7f6a |
fix(settings): stage Advanced model spec until Download is confirmed (#908)
* fix(settings): stage Advanced model spec until Download is confirmed Picker changes in Speech Model -> Advanced staged a spec but immediately triggered a model download for every intermediate selection. Gate the auto-apply download path so a missing model only stages: a Download action on the model info card now runs apply_settings() as the single explicit confirmation. Simple-mode questions keep the instant apply-and-download flow. Fixes #894 * fix(settings): repaint model info card after a completed download The post-download paths repopulated pickers and resynced the engine but never repainted the info card, so a finished download left the amber status and the Download action visible for a model already on disk. The cancel path already repainted via the idle resync; give success its own. * fix(settings): keep staged spec through picker rebuilds and save non-model edits * fix(settings): persist non-model edits without a model-reloading reconfigure * fix(settings): live-apply non-model edits, keep whispercpp params saved-only while staged * fix(settings): live-set changed VAD/timeout attrs without reconfigure while staged --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
775863fd30 |
feat(settings): add proxy support for model downloads and update checks (#909)
* feat(settings): add proxy support for model downloads and update checks Add a Proxy page to Settings (above About) with Off / System / Manual modes. Manual mode supports SOCKS5 and HTTPS (HTTP CONNECT) proxies with optional username/password auth; the password is stored in config.json in plaintext like the other settings. System mode honors the *_proxy environment variables and falls back to the GNOME org.gnome.system.proxy manual configuration. The resolved proxy is applied to every in-app outbound path that fetches models or checks updates: the shared requests download path in recognition_manager (_stream_model_download, used by the whisper.cpp, whisper, Vosk, Parakeet and faster-whisper downloaders) and the release checks in update_checker. A small Test Connection button on the Proxy page verifies reachability through the configured proxy. SOCKS5 needs PySocks, added as a runtime dependency; uv.lock, the requirements/*.txt exports and the Flatpak dependency manifest are regenerated by just lock (with a fix in sync_flatpak_deps.py for packages like PySocks that publish more than one universal wheel). Fixes #655 * fix(proxy): bracket IPv6 hosts, escape test errors, declare pysocks for AUR --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
442a646a57 |
feat(history): add opt-in JSONL persistence for transcripts (#907)
* feat(history): add opt-in JSONL persistence for transcripts Adds history.persist (default off) mirroring the bounded snippet list to history.jsonl under the XDG data directory so transcripts survive restarts. Entries carry timestamp, duration, language and model when known. Clearing history truncates the file, turning persistence off deletes it, and disabling history removes it so nothing is retained anywhere. Settings gains a Keep on Disk switch and a Clear button in the Transcription History group. Fixes #758 * test(history): rebind real tempfile when siblings mock sys.modules test_recognition_manager and test_speech_recognition replace sys.modules["tempfile"] with a MagicMock at import time, so the new persistence tests were sharing one mock-named store path in full-suite runs. Guard the TemporaryDirectory binding the same way test_whisper_model_info already does. * fix(history): sweep stale temp files, survive damaged stores, and keep session fields on late commits * fix(history): reject lone-surrogate records and survive encode errors on write --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
ff57244dce |
ci(install): gate the remote install path end to end (#906)
* ci(install): gate the remote install path end to end A curl|bash user takes a different path than the local install gate covers: bootstrap outside a checkout, tag selection, clone/fetch of the selected tag, then handoff to the tagged installer with the original arguments. Add scripts/install-remote-test.sh, which publishes HEAD as a tag in a local mirror, runs install.sh against it twice (fresh clone, then fetch+reset of the existing clone), and fails closed when the clone's HEAD is not the selected tag commit. install.sh gains a VOCALINUX_REPO_URL override so the mirror can stand in for the public remote. Wire a single-distro job into the distro matrix and a just remote-install-gate recipe. Refs #701 * ci(install): publish the gate tag as an orphan from HEAD's tree CI checks out with fetch-depth 1, and git refuses to push or fetch a shallow boundary commit into a ref. Build the tag on an orphan commit created from HEAD's tree instead, and make tree equality the fail-closed check that installer, modules and exports are this commit's files. * test(install): force the re-run arm to prove fetch and reset Review feedback on the remote-install gate: the second run reused a clone already on the tag, so a broken fetch or reset would still pass. Move the clone onto a descendant commit and delete its local tag ref before the re-run, so only a real fetch + reset restores them. Also list the new recipe in AGENTS.md's command guide. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
6a6cc93919 |
feat(tray): remove Start on Login and About from the tray menu (#905)
* feat(tray): remove Start on Login and About from the tray menu
Both entries already live in Settings: the autostart switch is in the
general page and About is a dedicated sidebar page, so the tray items
are redundant. View Logs stays.
Removes the Start on Login checkbox item along with its
_update_autostart_checkbox/_on_autostart_toggled wiring and the now
unused _add_menu_checkbox helper, plus the About item and
_on_about_clicked. _show_settings_page("about") stays for the Update
Available item. autostart_manager is untouched since Settings uses it.
Fixes #654
* fix(settings): sync autostart switch with the real desktop entry
The tray's removed _update_autostart_checkbox also reconciled
general.autostart with the actual login desktop entry at startup. Move
that check into the settings load so the Start on Login switch still
reflects reality when the entry changes outside the app.
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
|
||
|
|
26c4f9052b |
fix(settings): place test dictation output above its controls (#904)
Reveal the transcription readout at the top of the sidebar footer so it grows into the empty sidebar space above the Idle row instead of pushing the Test Dictation button down when it appears. Fixes #677 Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
4b7d57c077 |
feat(packaging): build thin .deb and .rpm and attach them to releases (#913)
* feat(packaging): build thin .deb and .rpm with nfpm and attach them to releases Adds packaging/native: an nfpm config that produces a .deb and an .rpm from the release wheel. The packages stay thin - Python, GTK, PyGObject and the typelibs are distro Depends; only pywhispercpp and pynput are vendored because the first is unpackaged on both distros and the second is missing on Fedora. The Debian 12 build image and the nfpm download are pinned in tool_checksums.txt. just native-packages builds both in the pinned container, and native-smoke installs the built package per distro: debian:12 and ubuntu:24.04 exercise the .deb, fedora:42 the .rpm, then each runs vocalinux --version plus an import smoke of the app and the vendored extension. unified-pipeline.yml gates on the same three-distro matrix, and release.yml attaches both arches to the release and covers them in SHA256SUMS and the provenance manifest. Refs #600 * fix(packaging): preserve launcher args and document Wayland input group The launcher joined $* into an unquoted exec, re-splitting file paths that contain spaces before argparse saw them. Exec "$@" directly and re-quote each argument for the inner shell of 'sg input -c'. On Wayland the packages could not grant the 'input' group membership evdev hotkeys need; name the usermod step and its caveat in INSTALL.md and the release notes. * test(packaging): add return types to the native packaging tests --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
d53300841d |
fix(appimage): reuse the Vulkan pywhispercpp build across CI runs (#903)
* fix(appimage): reuse the Vulkan pywhispercpp build across CI runs The x86_64 Build AppImage step spends most of its time recompiling pywhispercpp with Vulkan. That compile depends on the base image, tool_checksums.txt, and the pywhispercpp pin, not on the commit. Cache the native tree under one key shared by CI, Nightly, and Release. * fix(appimage): reject a partial Vulkan cache and key the build tools A failed copy could leave a tree that still contained libggml-vulkan.so, and the next run would reuse it. Check each copy, and keep the previous cache until the new one is in place. The cache id now also covers the pinned cmake and ninja export and the Vulkan compile flags. The hit path is what the build calls, and a test runs it with a stub compile command. * chore(ci): rerun AppImage against the saved Vulkan cache The review fix changed the native cache id, so run 37586979294 compiled pywhispercpp again and saved appimage-native-X64-ebf478b87986e2f6b9d80c97c2f5869fe3120c2bcd48f5d8f5a65fe06ec03015. This commit leaves the base image, tool checksums, pywhispercpp pin, installer tool pins, and Vulkan compile flags alone. |
||
|
|
af1029dc60 |
fix(evdev): skip pointer-motion devices in keyboard discovery (#902)
* fix(evdev): skip pointer-motion devices in keyboard discovery find_keyboard_devices() treated any device with a nonzero EV_KEY bitmap as a keyboard, which includes Logitech mice (G502, MX Master 3) whose button interfaces report key bits. The hub then grabbed them (EVIOCGRAB) and re-emitted every event, including EV_REL pointer motion, through a paired uinput clone — piping all cursor movement through the reader thread and showing up as periodic cursor stutter. Discovery now excludes devices that report pointer motion (REL_X+REL_Y or ABS_X+ABS_Y) in both the /proc/bus/input/devices parser and the evdev.list_devices() fallback. Wheel-only devices (REL_WHEEL) and real keyboards are unaffected, and a mouse's separate keyboard interface node keeps working for shortcuts. Fixes #900 * test(evdev): annotate new device-discovery tests * chore: drop accidentally committed pr_body.md --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
758f640787 |
fix(uninstall): stop deleting files in the caller's directory (#901)
* fix(uninstall): stop deleting files in the caller's directory The curl uninstall command runs uninstall.sh from $PWD, and the script treated that directory as a source checkout. It deleted venv, build, dist, and Python bytecode there, then reported success. Those paths are removed only when uninstall.sh itself lives in a Vocalinux checkout, and only inside that checkout. Fixes #897 * fix(uninstall): keep dev metadata and drop the false cleanup error Tests put a pgrep and kill stub first on PATH, so they cannot signal a Vocalinux process that is already running. egg-info cleanup stays under src/ and the checkout root, and the bytecode walk prunes .venv. Artifact cleanup runs before the curl clone is deleted, so an uninstall from ~/.local/share/vocalinux-install no longer prints an error and then success. |
||
|
|
96e012d9bd |
fix(evdev): keep key suppression when /dev/uinput is write-only (#893)
* fix(evdev): keep key suppression when /dev/uinput is write-only python-evdev opens /dev/uinput O_RDWR, but the kernel only needs write access for the uinput setup ioctls and event injection. Installs that still carry the original 80-dotool.rules line (MODE=0620, write-only for the input group) therefore fail UInput() with EACCES and fall back to ungrabbed monitoring, where the dictation shortcut is detected but also reaches the focused application. Retry the clone on an O_WRONLY descriptor via a small _WriteOnlyUInput subclass so suppression keeps working there (refs #871). Also populate _withheld_modifier lazily in _event_is_shortcut: the AltGr-replay machinery was dead code because nothing created the per-fd map, so a withheld modifier press was never replayed and composition (e.g. RightAlt+e) lost its modifier. * fix(evdev): byte-truncate clone names and cover the write-only path - _clone_device_name now truncates the encoded name by bytes: the kernel limit is 79 bytes, and a long multibyte device name could push the " (vocalinux)" suffix past the cut so rescan would not skip the clone. - _WriteOnlyUInput closes the fd explicitly when _prepare_events fails before the _uinput calls; the helpers close it themselves on any later failure. - Tests now instantiate the real subclass with mocked OS/native calls (open flags, setup sequence, write/write_event/syn ordering, fd cleanup, name truncation). --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
4497bfae6c |
fix(logs): let the View Logs dialog close via titlebar-X (#892)
* fix(logs): let the View Logs dialog close via titlebar-X _on_response only destroyed on ResponseType.CLOSE and ignored DELETE_EVENT, so the Logs window could not be dismissed with the window-manager close button — unlike Settings, which handles both. A window force-destroyed behind the dialog then crashed quit with a BadWindow error instead of an orderly shutdown. * test(logs): cover _on_response behavior; annotate its signature --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
d11bd1bf0d |
feat(post-processing): pipe transcriptions through an optional postprocessing script (#479)
* feat(post-processing): pipe transcriptions through user-defined script Add a configurable post-processing step that passes each transcription result to an external executable via stdin and uses its stdout as the text to type. Enables custom transforms (formatting, LLM rewriting, etc.) without modifying Vocalinux. Falls back to original text on script failure or timeout. * test(post-processor): add unit tests and extract apply_post_processing Move config-reading logic from main.py closure into apply_post_processing() in post_processor.py so it can be tested independently. Add 11 unit tests covering PostProcessor.process() and apply_post_processing(). * docs(post-processing): document post-processing script feature * fix formatting of one file * fix(post-processing): run the user script with a stripped host environment Rebase compat: the host_process AST guard requires every subprocess spawned for a host binary to pass env=host_env() so an AppImage's LD_LIBRARY_PATH/GI_TYPELIB_PATH/PYTHONHOME do not leak in. Also give apply_post_processing() a typed ConfigManager parameter and keep text_callback_wrapper's local variable typed as str for mypy. * fix(post-processing): preserve paragraph breaks and tighten script handling Take script stdout verbatim instead of rstripping every trailing newline, so the "new paragraph" voice command's break survives a passthrough script. A single trailing newline is still dropped, but only when the transcription had none — it is almost always just the line terminator echo/print-style tools append. Also stop writing transformed output to the log (it can carry sensitive dictation into retained logs), catch only the expected process/OS/encoding failures instead of every Exception, and add the missing signature annotations. * fix(main): run the post-processing script off the recognition thread A user script may take seconds per segment, and text callbacks run on the recognition thread that drains the bounded audio-segment queue; stalling it drops queued dictation once the queue fills. Submit post-processing and injection to a dedicated single-worker executor so the backlog waits instead of losing speech, while keeping injection order. On timeout or failure the worker falls back to the original text, and an unexpected error is logged with a traceback and still injects the original rather than silently dropping the segment. Also pin the callback tests' get_str mock to no-script so they exercise the synchronous path deterministically, and add a callback test proving transformed text (including preserved paragraph breaks) reaches the injector. * docs(post-processing): annotate new callbacks and clarify stdout contract Add the missing type annotations on the new Settings page callbacks per the repository guide, and document that script stdout is used verbatim (a lone trailing newline added by line-oriented tools is dropped only when the transcription had none) and that scripts run on a dedicated worker. * test(main): annotate new post-processing callback tests * fix(main): bind queued dictation to its target app and drain worker on quit Route every transcription segment through the single post-processing worker (a pass-through when no script is configured) so clearing the script path mid-queue can no longer let a later segment overtake an earlier one still running. A segment that can sit in the backlog — a script is configured or a job is still running — is bound to the application focused when it was dictated; the worker drops it if focus has verifiably moved to another application, so delayed dictation cannot land in the wrong app. Wire the worker into the tray quit path via an on_quit hook: it clears the acceptance flag, cancels pending jobs, and waits — bounded by the script's own timeout — so nothing injects after the injector stops. * fix(main): keep voice actions ordered and probe focus off the recognition thread - Queue voice-command actions on the post-processing worker so an action can no longer overtake dictated text still waiting to be injected. - Move the submit-time focus probe onto a dedicated executor; the recognition thread only submits the probe, and the job joins the result at inject time, so compositor tools can no longer stall consumption of the bounded audio-segment queue. - Re-check the quit flag after the inject-time focus re-probe, then shut both workers down without waiting on a running script so quitting does not freeze on the script timeout. * fix(main): capture submit-time focus on dedicated threads and serialize quit - Replace the shared focus-probe executor with a fresh daemon thread per submission so a slow earlier probe cannot delay a later one past the focus change it is meant to capture; jobs read the result off a one-slot queue at inject time. - Give queued voice actions the same submit-time focus binding as text segments so a queued shortcut is dropped instead of fired into a newly focused application. - Hold injection_lock across the accept-check plus inject/action call so quit waits only for an in-flight injection instead of racing it. - Queue the IDLE reset of last_injected_text behind pending worker jobs so a queued 'delete that' still sees the text it targets. * fix(main): keep actions instant on a free worker and bound quit wait - An action now starts a submit-time focus probe only when it actually queues behind other jobs; with a free worker there is no delay window to bind against, so the probe's compositor calls are skipped and the action stays immediate even when a script is configured. - Probe and executor submission now happen inside pending_jobs_lock so queue order matches callback order — a job that saw an empty queue can no longer slip behind one that arrived while it was submitting. - Every inject-path subprocess call (xdotool window/key/type, wtype and ydotool type) is bounded by a timeout, and the quit hook's injection_lock wait is capped, so quitting can no longer freeze on a stalled injector subprocess. * fix(main): bind every action to submit-time focus without slowing it down - Every voice action now starts a submit-time focus probe: submission never guarantees immediate execution, so skipping the binding left a focus-change window in which a shortcut could fire into another app. The worker waits on the probe for at most one second — milliseconds on a healthy desktop — so immediate actions stay immediate while a genuinely stalled probe falls back to the injector's own targeting. - Quit waits for an in-flight injection without a cap again: every inject-path subprocess is individually bounded, so the wait always ends with the injection and a long transcription is never cut off. - The wtype/ydotool typing timeout is now derived from the configured per-key delay (a generous multiple), so a legitimately slow type can never be cut mid-text; a timed-out type no longer falls through to the clipboard fallback, which would duplicate the partial fragment. * fix(main): drop unverified actions and make quit-time injection abortable - An action whose submit-time focus probe cannot answer inside its one-second bound is now dropped instead of running unverified: a stalled probe can no longer leave a shortcut pointing at whatever application happens to be focused. - Quit asks the injector to abort in-flight injections, and injection paths honour it: the xdotool chunk loop stops between chunks, the wtype/ydotool type call runs as a polling Popen that can be killed, and paste/shortcut/backspace entry points exit early. The quit hook still waits for an in-flight injection, but the wait is bounded by at most one already-running subprocess call — never the injection's full length — and no subprocess is ever stopped mid-call by a timeout cap on the wait itself. - Annotate the remaining untyped test helpers. * fix(main): wait out slow focus probes; restore run() in wayland typing - An action no longer races its submit-time focus probe on a one-second bound: the job waits for the probe's own answer, whose compositor calls already carry per-call timeouts and short-circuit absent tools. A slow but living probe delays the command instead of discarding it, so a valid "undo" can no longer be lost to timing alone, and focus verification still decides every drop. - Wayland typing goes back to subprocess.run so tests keep intercepting the same entry point. Abortability is preserved by typing in 200-character chunks with the shutdown check between calls — a whole-string call could not be interrupted, but no single chunk holds the injector for more than its own scaled budget. - The abort flag gains a class-level default so injectors built without __init__ in tests still answer the checks; each real instance gets its own event in __init__. - Update call-shape assertions for the typed-call timeout and finish annotating the test methods touched by the callback refactor. * fix(inject): never retype text a failed chunked call already sent - _PartiallyTyped carries the confirmed-delivered prefix length out of the chunked type loops, so every downstream fallback continues from the untyped remainder: the Wayland-to-xdotool switch types only the tail, and the clipboard fallback copies only the tail, never the whole string a second time. - The xdotool retry loop gains the same accounting — a retry now resumes at the first untyped chunk instead of restarting at the top and duplicating everything the failed attempt already delivered. - Annotate the remaining test signature the repository guide requires. * fix(inject): report partial typing as failure even when clipboard lands A chunked type call that dies mid-text leaves only a prefix on screen, and the remainder-fallback keeps it that way: it copies the tail for a manual paste but now returns False, so the caller no longer records the full transcription as injected and a following "delete that" cannot erase text that was never typed. * fix(inject): keep deletion state aligned with delivered text inject_text now records how many characters a call confirmed on screen as last_typed_count: the whole length on success, the typed prefix after a partial failure, -1 when the delivered amount is unknowable. The transcription caller uses it to keep last_injected_text honest — a following "delete that" erases the typed prefix or nothing, never the previous segment or text that never arrived. * fix(inject): never replay input a timed-out portal job may have delivered - RemoteDesktopPortalError.delivered=None marks an unreported job's count as unknowable; all three portal fallbacks demote and skip instead of replaying, and text injection surfaces as TimeoutExpired so no full-text clipboard copy is offered - _PartiallyTyped counts raised after a portal demote are rebased onto the original text (typed_prefix) so the caller clips the real prefix - evdev: a consumed pure-modifier press is withheld and replayed ahead of the first non-target press, so holding RightAlt for AltGr composition still reaches the application - history: extend_latest gains an expected_latest CAS guard so a late segment from an older worker merges only into its own session's snippet, never into a newer session's entry - quit path aborts in-flight injections before waiting on the lock so quitting cannot freeze behind a long transcription's typing budget - tests: stop _ensure_ydotoold leaking a real Popen in Wayland tests * fix(shortcut): replay withheld modifiers across devices and after SYN_DROPPED - Replay withheld modifier presses from every device, not only the fd the character arrived on: split keyboards deliver AltGr and the character on separate devices, and a per-fd check forwarded a plain character. - Let the post-SYN_DROPPED resync run when only withheld modifiers exist for the fd, so a press whose release the dropped burst ate is pruned instead of replaying a stale modifier on the next ordinary key. * fix(history,pads): keep late segments pad-bound deliveries and pad deletions correct - Track the worker behind the ended session's snippet so late segments from an older worker cannot merge into a newer session's entry - Skip the focus-change drop for dictation-pad segments and pad-bound editing commands, which never target the focused application - Refuse 'delete that' when a manual pad edit blurred the segment boundaries instead of deleting the stale recorded length * style: drop unneeded nonlocal for mutated worker set * fix(shortcut): share one evdev device layer across shortcut listeners Each KeyboardShortcutManager built its own EvdevKeyboardBackend, and on evdev every backend opened and grabbed the same physical keyboards: the first grabber received all events and the rest received none, so per-language shortcuts could never fire on Wayland. All backends now bind to a process-wide EvdevDeviceHub that opens each keyboard once, owns the grabs and the paired uinput clones, and fans every event out to each registered engine. An event is re-emitted to applications only when no engine consumed it, so suppression semantics stay the same while per-engine matching state (combo progress, withheld modifiers, swallowed presses) remains independent. * fix(history): own late segments per worker and decide pad routing once - ended_worker_snippets maps each ended session's worker to its own (snippet, commit epoch): late deliveries merge into their session's entry however many sessions have committed since, so one session's stragglers never split across Recent Snippets entries. - The leftover-segment commit reads the live history epoch inside the session lock: a mid-session clear() no longer makes the stale ended epoch refuse valid post-clear dictation. - The pad-bound routing decision is captured once per job and passed to the injector/action handler, so a toggle flip between the focus check and delivery cannot reroute text or shortcuts into the focused app. - Dead recognition workers are pruned from the tracking set and map so long runs don't accumulate finished threads. - The custom-terms switch now suppresses GTK's own state-set on save failure so it shows the value that actually persisted. * fix(shortcut): serialize evdev device layer teardown against restart The last listener's unregister released the engine lock before joining the monitor thread and closing devices, so a new listener registering in that gap opened keyboards the stale teardown then closed — it stayed active but dead, and briefly grabbed keyboards stopped forwarding input. A lifecycle lock now covers the whole close/open transition in register, unregister, and the monitor's unexpected-exit teardown, so a cold start can only open devices after the previous generation finished closing (also making the new generation's grabs succeed). A generation stamp tells a monitor thread that outlived its join() its incarnation is over even when running has gone True again. * fix(injection): log focus-probe failures and narrow clipboard fallback catch A crashed focus probe turned into an unknown result with no logged error, indistinguishable from unavailable focus information; the partial-typing clipboard fallback caught every exception. Log the probe failure with context and narrow the clipboard catch to the exceptions the fallback path can actually raise. * fix(shortcut): join the evdev monitor outside the lifecycle lock If the monitor exited unexpectedly while the last listener unregistered, its cleanup queued on the lifecycle lock that unregister() held for the join — stalling shutdown for the join's full 2s timeout. The close now finishes inside the lock and the join waits after it is released, so the monitor's own cleanup can run and exit instead of queueing behind it. Closing before the join also dies the monitor's read/select on the closed fds right away. A _closed gate on _open_keyboard_device keeps a rescan from opening a device between detach and close that nothing would ever close. * fix(injection): narrow focus-probe catch to the raisable exception set get_focused_window() reports Optional and never raises; the catch now covers only what can realistically escape the queue write and logs it with context, keeping the waiting job released. * fix(shortcut): tag evdev device fds with the generation that opened them A monitor that outlived join() could still touch a newer generation's devices: the loop re-checked the generation only at the top, so mid-iteration it could dispatch from, forward to, or remove devices a restarted listener had just opened. Every fd is now tagged with the generation that opened it, and the tag is verified at each point a monitor could mutate shared state — fd lookup for dispatch, clone forwarding, forwarder release, device removal, and rescan opens — while dispatch re-checks the generation between events and the per-fd loop breaks on it. Cold starts bump the generation before opening so fresh devices are tagged for the new generation. Devices an older generation opened stay tagged stale, so its reader leaves them alone. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
dca937fc21 |
feat(dictionary): combined custom dictionary with corrections (#890)
* feat(dictionary): combined custom dictionary with corrections Rebased onto latest main (conflict-resolution squash for PR #818). * fix(dictionary): stabilize the terms and corrections file contracts - corrections_path() stays at the fixed config_dir() location promised by the file contract instead of following a custom or --dictionary-file terms path, which could strand or duplicate corrections; a leftover file beside the terms file is still read once and copied over. - _atomic_write() preserves the replaced file's permission mode instead of resetting it to the process umask. - build_initial_prompt() streams terms and stops at the configured cap instead of reading and processing the whole file for every segment. * fix(settings): restore the selected dictionary pane after a search Searching can auto-switch the Custom Dictionary management pane to reveal a match, but clearing the query only restored widget visibility. Record the pane selection in the search baseline snapshot and restore it alongside the previous page so users return to the pane they chose. * fix(dictionary): close the remaining custom dictionary file-contract gaps - A --dictionary-file session override no longer imports a corrections file found beside the override into the standard config directory; the override scopes to terms only. - Terms reads validate the whole file before yielding: corrupt UTF-8 mid-file now yields no terms at all, matching the invalid status Settings reports instead of leaking a partial dictionary to recognition. - The terms read is bounded by MAX_TERMS_FILE_BYTES (1 MiB); a larger file is invalid everywhere with a dedicated status instead of being read on every segment. * fix(dictionary): keep usable terms prefix on oversized files and legacy corrections under overrides * fix(dictionary): cap yielded terms and keep added terms visible on oversized files * fix(dictionary): keep added terms visible without dropping scanner-owned lines * fix(dictionary): keep oversized terms manageable and honor mid-search pane switches - terms list gains a Show all / Show first 500 toggle; adding a term beyond the cap auto-expands so the new row renders with a Remove button instead of being invisible until the file is hand-edited - a manual dictionary pane switch during an active search drops the saved pre-search pane so clearing the query no longer rolls back the user's newer selection - annotate the new settings tests and type model_info so the parakeet branch stops adding fresh mypy errors * fix(settings): do not auto-expand terms on add past display cap Rebuilding up to thousands of term rows on the UI thread stalls the dialog; the overflow button already shows the true count, so the added term stays one click away. * test(dictionary): stub segment_callbacks on bare manager for history dispatch * fix(dictionary): stream oversized terms files when building the prompt build_initial_prompt decoded the whole 1 MiB read window before every transcription segment even though it only needs the first max_words terms. _iter_terms_for_prompt now reads oversized files line by line and stops at the prompt limit, keeping the same contract: complete leading lines only, invalid UTF-8 inside the usable window yields nothing. * fix(history): only the ended session's worker may extend its snippet A late segment could extend the newest history entry regardless of which session produced it, so a worker finishing after the next dictation session leaked its text into that session's entry. Track the worker that owned the ended session and require late segments to arrive on the same thread before merging; anything else forms its own snippet. * fix(speech): transcribe queued segments the reload worker never drained The recognition worker can exit during stop_recognition's bounded audio-thread join, so the released key's final buffer lands in the queue after the worker is gone and was silently dropped when the reload teardown replaced the queue. Drain whatever remains before swapping it — except on capture failure or an explicit cancel, which still discard the session's leftovers. * fix(settings): build term rows in idle slices and pin hidden additions Two problems shared one cause. Rebuilding up to 4096 rows synchronously stalled the dialog, and a term appended past the 500-row cap reported success while staying invisible. Rows now build in bounded GLib idle slices guarded by a refresh token, and a saved term outside the visible window is pinned to the top of the capped list until it disappears or becomes visible on its own. Adding under "show all" scrolls to the new term instead. * test(engine): annotate new test method return types * fix(ci): match a candidate row that leads the latest track block snapcraft status normally prints candidate as a continuation line under the latest track (channel in column 1). If candidate ever leads the track block it rides the track line instead (channel in column 3), and the resolver missed it and refused to promote. Match both layouts in the resolve and the pre-release re-verify. * fix(dictionary): bound prompt reads and scroll to landed terms - _iter_terms_for_prompt now reads each line via readline() capped by the remaining byte window, so an overlong line can never pull more than MAX_TERMS_FILE_BYTES into memory - the prompt path keeps decoding every complete line inside the usable window after the term limit is collected, so invalid UTF-8 anywhere in the window still yields no terms and matches the reader's invalid status - under "show all" Settings scrolls to the index where an added term landed (prepended or appended), verified against the row's label, instead of always scrolling to the end * fix(history,reload): keep late-segment ownership and drain stamped tuples - Track session_workers_seen so a worker from an earlier session can never merge into a newer snippet while a session is open - Unpack (segment, language) queue tuples in _transcribe_queued_segments; passing the tuple as audio raised TypeError and lost final speech - Refuse 'delete that' when a manual pad edit blurred segment boundaries - Translate term-row coordinates into the scrolled card's space so the scroll actually reaches rows below the fold - Type _on_dictionary_pane_changed's varargs * style: drop unneeded nonlocal for mutated worker set --------- Co-authored-by: Graham <graham@thegeeky.ninja> |
||
|
|
047b379bd2 |
feat(audio): add PipeWire capture path with system-audio sources (#889)
* feat(audio): add PipeWire capture path with system-audio sources Enumerate PipeWire sinks via pw-dump and expose their monitors as "System audio: <name>" capture devices at negative device indices (-2..), alongside the existing PortAudio microphones. Recording, device test, and stream-reconnection paths route to a pw-record-backed stream shaped like a PyAudio stream, so the VAD and silence-detection loop is unchanged. Playback ducking is skipped when the selected source is a monitor, since ducking the default sink would also duck the captured audio. Closes #751 * test(audio): restore stashed numpy instead of re-importing it test_audio_input imports numpy lazily; deleting the MagicMock that earlier test modules leave in sys.modules re-imported a second numpy module object whose lazy submodule attributes (linalg, testing) were unpopulated while sys.modules kept the first object's entries. The next test file to reach for numpy.testing hit a __getattr__ import loop (RecursionError in TestDownmixToMono on CI). Restore the conftest-stashed _vocalinux_real_numpy instead, matching the pattern tests/test_recognition_manager_device_config.py already uses. * fix(pipewire): drop PyAudio requirement for PipeWire sources, rebind source after reconnect * docs(pipewire): restore PipeWireCaptureSource class docstring * fix(audio): initialize PyAudio for mic reconnection after PipeWire session A session started on a PipeWire sink never creates a PyAudio instance; when capture later reconnects onto a PortAudio device the source needs one to reopen, and a failed open dereferenced None calling terminate. * fix(audio): harden PipeWire source resolution, reads, and cleanup - resolve stored names by node.name/description; a stored name that no longer resolves never falls back to a positional index - require pw-record before listing system-audio sources in the picker - bound pw-record chunk reads with a stall watchdog so stopping cannot block on a wedged stream - close the reconnection probe stream when its verification read fails - log cleanup failures instead of swallowing them; narrow exception handlers in enumeration and PyAudio reconnection init - annotate test helpers and signatures per repo style --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
9898bb8dd9 |
feat(ui): in-app Dictation Pad for Wayland-safe dictation (#887)
* feat(ui): add in-app Dictation Pad for Wayland-safe dictation Dictated text can now be routed into a text box inside Vocalinux's own window instead of being injected into other applications — an explicit fallback for desktops where cross-app injection is unreliable or blocked (notably Wayland). The pad opens from the tray menu, captures dictation when enabled via Settings or its own checkbox, and lets the user copy text out by hand. 'delete that' removes the last segment in-pad. Closes #726. * fix(ui): consult live config for pad auto-reveal and Copy All The reveal gate used the controller's enabled flag, which only syncs on pad show or its own checkbox — enabling capture via the Settings switch while the pad was closed silently buffered dictation with no reveal. _apply_append now reads dictate_to_pad from the shared config. Also: Copy All copies the widget text (dictation plus in-pad edits), and re-showing the pad no longer does a wholesale set_text — the view stays incrementally in sync, so manual edits survive hide/reopen. * fix(ui): keep dictation pad capture and commands inside the pad Address all Greptile findings on PR #887: - Read the capture toggle from the shared ConfigManager instead of re-parsing config.json per segment, so a torn mid-write read can no longer fail-open and send dictation to the focused app - Sync edits made directly in the pad widget back into the controller, keeping delete semantics consistent with the visible text - Stamp queued widget operations with a generation tag so a Clear drops appends still sitting in the GTK idle queue - Route undo/redo, select all/line/word/paragraph and cut/copy/paste to pad-local implementations while capturing, so their shortcuts never reach whichever application holds focus - Track where the last segment was delivered so "delete that" follows the text rather than the current capture toggle - Annotate every new signature and add callback-level tests that pin the routing, queue, and undo behavior * fix(ui): keep pad controller, queued ops, and delete target in sync Manual edits could overwrite queued dictation in the controller because GTK input events outrank idle callbacks; the changed handler now replays pending widget ops before syncing the view. The controller also tracks dictated segments across undo snapshots, so a pad undo pops the "delete that" target instead of leaving a stale length. Catches in the pad are narrowed to the GTK/config error set per repo style, and missing type annotations are added. * fix(ui): close flush races in pad idle queue and delete retarget Flushed idle ops stayed scheduled with GLib and ran a second time, duplicating appends; each queued op now only acts while its entry is still pending. A queued full-view refresh could also erase a manual edit that landed first, so the changed-handler flush now drops stale set-text ops instead of replaying them. And clearing the pad destination on undo-to-empty kept a redo from restoring the "delete that" target; the destination flag is preserved so redo re-arms it. * fix(ui): replay pad idle ops before edits land, not after Flushing in the changed handler still let queued appends leapfrog the keystroke that fired it, and a stale dispatch could cancel a later queued op with identical text. Pending entries now match by identity, and the replay moved to insert-text/delete-range — signals that fire before the edit is applied — so queued dictation and deletes keep true chronological order against manual edits. * fix(history): target late segments to their own snippet by id A straggler from a just-ended session could merge into whichever snippet was newest when it arrived, and audio captured before a session opened was attributed to that session while decoding on its worker. - TranscriptionHistory.add returns the snippet id and the new extend_entry() extends a specific entry, replacing the positional extend_latest target in the late-segment path - session_started_floor stamps when a session opens; in-session attribution also requires the capture to postdate it - test_capture_ceiling closes the mic-test window at callback restore so test speech still decoding can no longer leak into history * fix(settings): keep deferred edits from overwriting a newer choice The deferred-persist filter treated "config still equals the baseline" as untouched, so a reopened dialog that applied a key back to its baseline value could still lose it to the closed dialog worker. - Record the generation of the apply that last wrote each advanced key; a pending edit now survives only when no newer apply wrote that key - A worker apply whose snapshot a newer apply predates now skips its reconfigure and save entirely, so collection order — not lock order — decides which snapshot wins * fix(settings): skip stale applies before stopping dictation The superseded-apply check ran after stop_recognition, so a stale worker still interrupted a live dictation session. Recognition now stops inside the apply lock, after the generation check. * fix(shortcuts): keep language edits from breaking dictation Two per-language shortcut fixes (#805): - A binding edit landing while a session is live no longer stops recognition. The dialog's per-keystroke persist still saves the new binding, but the tray defers the listener rebuild to the next IDLE transition, so a held push-to-talk keeps its release callback and a toggle session is never cut off mid-utterance. - A row edited onto a key another row holds can no longer displace that row's binding. Each row's remembered shortcut is reserved for its own row before claims run in row order, so the stealing edit is rejected and the held binding survives. - Persists that resolve to the already-saved bindings (mid-edit keystrokes, rejected duplicates, reverted fields) skip the save and the listener rebuild entirely. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
dbe0215aad |
feat(shortcuts): add language switching shortcuts, fix PTT races, and add dictation history (#880)
* feat(shortcuts): per-language dictation shortcuts (#805) Bind extra keys that start dictation in a specific catalog language for one utterance, then restore the configured language. Bindings live in shortcuts.language_shortcuts as [{shortcut, language}], normalized against the language catalog and the backend's shortcut parser. The tray spawns one KeyboardShortcutManager per binding in the active mode; toggle presses route through start_recognition_with_language, and push-to-talk arms press/release the same way. The manager keeps the override one-shot: it applies before the utterance starts (outranking follow-keyboard-layout) and unwinds on the next IDLE/ERROR transition or when a mid-dictation reconfigure picks a new language. Engines that cannot re-language without a reload (VOSK, Parakeet, English-only whisper.cpp) refuse with the error cue and a notification instead of dictating in the wrong language. Settings gains a Language Shortcuts group under Shortcuts: each row is a language picker, an editable key entry, a Record button that retargets the shared key-capture, and a remove button; edits persist immediately and rebuild the live listeners. A non-dict shortcuts section now rebuilds instead of crashing config load. * fix(shortcuts): class-level default for language shortcut listeners TrayIndicator stubs built with __new__ (e.g. test_suspend_handler) skip __init__ but still run _quit(); give _language_shortcut_managers a class-level default so teardown iterates an empty list. * Fix Greptile review findings on per-language shortcuts (#805) - Bind each dictation session's language for transcription workers so the one-shot restore cannot rewrite a queued segment's language - Judge a shortcut's capability against the model the layout refresh will serve (whisper.cpp sibling swap) and refuse requests the engine drops - Add _oneshot_language_restore to the reconfigure snapshot and settle a revived pending restore on failure - Dedupe language bindings by gesture (modifier reordering, enter/return alias, pure-modifier side sets) instead of by string - Stop per-language listeners under external activation and never re-arm - Gate push-to-talk release on the binding that started the session, and stop a held hold before rebuilding listeners - Keep a row's last valid binding during partial edits, report rejected duplicates, cancel a capture aimed at a removed row, disarm the previous Record target, persist a recorded key once, and add the missing type hints * fix(shortcuts): resolve remaining Greptile race findings on PR #880 - Stamp each queued audio segment with the session language it was recorded under, so a worker that outlives stop_recognition's joins transcribes each utterance in its own language rather than whatever a newer session stored in _session_language. - Serialize push-to-talk press/release behind _ptt_lock: a release landing while start_recognition runs now waits for the owner claim instead of being dropped, so a quick key tap cannot leave the microphone running. - Add the missing return type hint on _stop_language_shortcut_managers. * test(shortcuts): stub _segment_started_at on bare manager stubs * fix(shortcuts): carry segment language as attribute, default language on bare managers * fix(shortcuts): keep (segment, language) queue tuples, stamp segment.language too * fix(settings): apply history toggle and limit to the live store --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
ca40f7d8b0 |
fix(text-injection): keep Alt+Shift/Win+Space layout switching on GNOME Wayland (#876)
* fix(text-injection): restore full GNOME XKB map on XWayland after inject sync_xwayland_layout_from_gnome wrote only the single active input source via 'setxkbmap -layout <one>'. That left XWayland with a one-group keymap: Alt+Shift 'grp:' toggles and the group index Mutter drives for Win+Space became no-ops for every X11/XWayland client until relogin. Apply GNOME's complete XKB map instead: every xkb input source in 'sources' order plus all 'xkb-options', and only when the live rules differ (which also keeps inject from resetting the active group). Refs #848 * fix(text-injection): harden XWayland repair against bad reads and non-GNOME sessions Three Greptile P1s on f005b6c: - _read_gnome_input_sources_key now returns [] for a successfully read empty list, reserving None for unreadable keys. _get_gnome_xkb_keymap bails when the xkb-options read fails instead of treating it as an empty list, so a transient gsettings failure can no longer make the repair clear live XWayland options via 'setxkbmap -option ""'. - _get_gnome_xkb_keymap restores the GNOME-session guard the previous source reader had, so a stale org.gnome.desktop.input-sources schema on another Wayland desktop is never applied to that desktop's XWayland server. - _query_xserver_xkb_rules catches OSError, not just FileNotFoundError: launch failures like PermissionError run inside inject_text's finally and must not surface after a successful commit. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
8ebc4ce2ba |
feat(diarization): per-speaker transcripts via TinyDiarize + --transcribe-file (#884)
* feat(transcribe): add TinyDiarize file transcription surface Catalog small.en-tdrz (hosted at akashmjn/tinydiarize-whisper.cpp, pinned commit) while keeping it out of every dictation-model selection path: its [SPEAKER_TURN] markup would type noise into the focused window. The checksum generator resolves side-repo models at their own pinned revision, parakeet-style. Add speech_recognition.diarization (strict-WAV fast path with ffmpeg fallback, tdrz transcription, speaker-turn parsing, timestamped formatting) and surface it through a tray Transcribe Audio File flow with on-demand model download plus a headless --transcribe-file CLI. TranscriptDialog offers copy and save of the diarized transcript. Closes #756 * docs(user-guide): document --transcribe-file * fix(transcribe): pass tdrz_enable as a pywhispercpp kwarg pywhispercpp's Model takes decode params as keyword arguments; a params= dict crashes on the C-level whisper_full_params object. Also let --transcribe-file report unexpected failures cleanly instead of dumping a traceback. * fix(tray): dismiss the tdrz download dialog on response * test(diarization): pin real wave/numpy/tempfile around sibling mocks Several test modules replace sys.modules entries with MagicMocks at import time and never restore them, so diarization's wave/numpy/ tempfile bindings resolved to mocks when this file ran after them in CI. An autouse fixture reimports the real stdlib modules per test, the same remedy test_audio_feedback already applies for wave. * fix(transcribe): verify tdrz model, preload libs, gate formats on ffmpeg - Hash the on-disk TinyDiarize model against the pinned digest before native code maps it, matching the dictation load path - Extract the bundled-lib preloader to utils/pywhispercpp_loader.py and preload before the lazy pywhispercpp import so the headless --transcribe-file path works on source-built installs without RPATH - Offer only WAV in the file picker when ffmpeg is absent; name the dependency in the filter label - Type the new tray/diarization callbacks; document --transcribe-file and the new modules in AGENTS.md * style(audio): finish type annotations on capture helpers and download path _setup_alsa_error_handler now declares Optional[Any] and its ctypes callback gets concrete parameter types; the remaining capture helpers take audio: Any (PyAudio has no importable type) and test_audio_input's device_index becomes Optional[int], which also clears the preexisting implicit-Optional error. _download_whispercpp_model gains -> None and its HF url is narrowed to str so the concatenation and download call stop adding new mypy findings. * style(tests): wrap long monkeypatch line in preload test * refactor(main): narrow transcribe-file errors and add file context * style(ui): annotate TranscriptDialog constructor * fix(diarize): log worker tracebacks, isort test helpers, document --transcribe-file --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
b4babcaeca |
feat(packaging): AUR -bin package publishing with per-arch digests (#879)
* feat(aur): add vocalinux-bin PKGBUILD shipping the AppImage Closes #817 * fix(aur): hard-dep injection tools, pin real digests, gate the -bin package * fix(aur): pin both-arch -bin digests and build the binpkg as builder updpkgsums in the publish action only regenerates the x86_64 sum array, leaving aarch64 stale after every tag; release.yml now pins both AppImage digests itself and publishes with updpkgsums off. The AUR gate also created $WORKDIR/binpkg as root after handing WORKDIR to builder, so makepkg could not write there. AUR.md now matches the published -bin package. * fix(ci): wait for aarch64 AppImage before publishing vocalinux-bin publish-aur only needed build-and-release, so on a tag the digest-pinning step could curl the aarch64 AppImage before build-appimage-arm64 attached it — a 404 that failed the job between the source and -bin publishes. Split the -bin bump/pin/publish steps into publish-aur-bin, which needs [publish-aur, build-appimage-arm64]: the source package still ships as early as before while the binary package only runs once both AppImages exist on the release. * fix(ci): ship -bin local sources and wait for the aarch64 asset on AUR publish Split the -bin bump/pin/publish into publish-aur-bin needing [publish-aur, build-appimage-arm64] so the digest-pinning step can no longer curl a 404 for the not-yet-attached aarch64 AppImage mid-release, and publish with asset_dir so the PKGBUILD's local sources (vocalinux.desktop, vocalinux.svg, LICENSE) reach the AUR repo instead of a lone PKGBUILD users cannot build. * style(tests): apply black formatting to aur packaging test --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
5c031e64fb |
feat(settings): bilingual dictation support with deferred settings edits (#424)
* feat: add configurable Whisper language candidates * fix(settings): defer language candidate edits and tolerate list config The candidates entry applied settings on every keystroke, restarting the whisper.cpp model per character, and edits typed while an apply held the guard were dropped so the saved value could differ from the entry. Defer application like the initial prompt: mark edits dirty and apply on page change or close; _finish_auto_apply re-applies edits that landed mid-apply, and _persist_pending_text_edits saves them if the dialog closed first. A JSON list in config also crashed the dialog on open because Gtk.Entry.set_text needs a string; populate the entry through the engine's _normalize_language_candidates so both stored forms render the same text. * fix(recognition): validate whisper.cpp language candidates before selection auto_detect_language's probability map names every language the model can return, but candidate selection scored configured codes against it with a 0.0 default, so a typo or "auto" could still win the max() and reach transcription. Only codes present in the probability map are eligible now; unsupported codes are logged and skipped, and when none qualify the unrestricted detection result is used instead of forcing a code the model never scored. Also add the missing input annotation on _normalize_language_candidates. * fix(settings): persist deferred text edits off the GTK main loop _finish_auto_apply runs on the main loop and called _persist_pending_text_edits synchronously, so reconfigure() restarted the model in-process and froze the UI when the dialog closed mid-apply. Run the reconfigure + config save on a worker thread, matching the normal apply path. Also annotate _on_language_candidates_changed's signature. * fix(settings): guard deferred saves and annotate new candidate tests * fix(settings): reconcile the engine to winning settings after a mid-restart apply * fix(settings): drop superseded deferred edits instead of saving them A dialog closed mid-apply stashes its pending text edits for a worker to persist after the running apply finishes. The worker's mtime check could not tell the in-flight apply from a newer one started in a reopened dialog, so the older snapshot could reconfigure the shared engine and save over the settings that won. Serialize applies and the deferred persist on a shared lock and order them with a generation counter bumped when an apply begins. The worker now skips reconfigure and the save entirely when a newer apply has begun, and skips the restart when the running apply already landed the same values. * fix(settings): merge deferred text edits per key instead of dropping them Closing the dialog mid-apply stashes the typed prompt/candidate values with the apply's generation. When a reopened dialog applies newer settings first, the generation check used to discard the whole stash — losing the closed dialog's edits even where the newer apply never touched those keys. Persist keeps a pre-edit baseline per key: after a newer apply lands, only keys it actually changed are dropped; the rest still reconfigure the engine and save, so a stale value can never reach the shared engine while untouched edits are no longer lost. * refactor(settings): narrow deferred-edit persist exception with context * chore: retrigger ci --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
b71164a214 |
feat(tray): add recent dictation snippets history menu 🤖🤖🤖 (#487)
* feat(tray): add recent dictation snippets history menu Add a "Recent Snippets" submenu to the tray indicator that lists recent dictation sessions. Clicking a snippet copies its full text to the clipboard for easy re-pasting; a "Clear History" entry empties the list. Snippets are recorded on recognition, independent of whether text injection succeeds, so text can be recovered even when injection silently no-ops (e.g. on some Wayland compositors). One snippet corresponds to one dictation session (everything said between start and stop); voice commands are not recorded since they take a separate callback path. History is kept in memory only (newest-first, capped, never written to disk) and clears on quit — a deliberate privacy choice for a tool that sees everything dictated. Adds a "history" config section (enabled, max_items) and matching Settings UI under General. Includes unit tests for the new TranscriptionHistory store. * fix(history): sanitize saved max_items to keep startup robust A hand-edited config.json can store a non-numeric or out-of-range history.max_items. Coerce the value on load (and in TranscriptionHistory itself) with a positive-int fallback so a malformed preference warns and falls back to the default instead of aborting startup or crashing the Settings dialog spin button. * fix(history): fold late-arriving segments into their originating session stop_recognition() only waits on the recognition worker for a bounded time before reporting IDLE, so a slow final segment can still fire its text callback afterwards — landing in the next session's pending snippet, or starting a phantom entry of its own. Track the open session (state transitions, its worker thread, and its pending segments) under a lock and file each segment by origin: while a session is open only its own worker's segments accumulate into it; a segment arriving after finalization extends that session's committed snippet via the new TranscriptionHistory.extend_latest(), and a segment from a session that produced nothing forms its own entry instead of polluting the next one. ERROR now also finalizes the session, so a partial result is still recorded and the tracker never wedges open. Covered by new tests driving the callback and state wiring directly: mid-session LISTENING/PROCESSING transitions, back-to-back sessions, late callbacks from a lingering worker thread, disabled history, and invalid saved limits. * style(ui): type the new history handlers in tray and settings * fix(history): keep cleared snippets from reappearing via late segments The recognition worker can still emit a final segment after the manager's bounded stop wait reported IDLE. Once the user cleared the history, that late text could not extend the now-empty history and was appended as a new entry — resurrecting snippets the user explicitly removed. Give TranscriptionHistory a clear epoch: clear() (and disabling, which clears) bumps it even when the store is already empty. add() and extend_latest() accept an expected_epoch and refuse stale epochs atomically under the store lock, so the check cannot race the clear. main.py tracks the epoch each session opened under and the epoch of the most recently ended session. Session commits and every late-segment write are guarded by them, so text produced before a clear — whether a session's whole snippet, its straggler segments, or leftovers committed on the next session's start — cannot re-enter history afterwards. * test(history): cover clear-epoch rejection and type new signatures Add regression tests for the cleared-history race: a late segment after clear() must not reappear, a session ending after a mid-session clear leaves no snippet, and add/extend_latest refuse a stale expected_epoch while unguarded writes keep working. Annotate the new signatures per the repo guide: _boot's extra_settings and return tuple, every test in the new TestSessionHistoryRecording and history-menu tray classes, and all methods of the history store tests. * fix(main): keep post-clear dictation by rebinding the session epoch * fix(history): drop the boundary segment when a clear lands mid-session * fix: scope dictation history to post-clear audio via capture-time stamps The engine now tags each queued audio segment with the monotonic time capture began and reports it to a new segment_callbacks hook. History recording drops segments whose capture started before the last clear, so pre-clear audio that decodes late can no longer resurrect cleared entries or leak into a later session, while text dictated after a mid-session clear is kept instead of being discarded by a stale epoch check. * fix(settings): keep Test Dictation speech out of transcription history The mic test swaps text callbacks for its preview, but history recording runs on segment callbacks, so test speech still landed in Recent Snippets. The test now saves and clears the segment callback list alongside the text list — restoring both on success and on the start-failure path. register/unregister_segment_callback also gain the missing return annotations and the engine gets symmetric get/set_segment_callbacks accessors. * fix(settings): floor test-dictation speech instead of swapping callbacks Instead of clearing the segment callback list during the mic test — which would have dropped leftovers from a still-finishing dictation — the engine exposes test_capture_floor: segments captured at or after the stamp are test speech and stay out of Recent Snippets, while earlier-session leftovers continue to file normally. * refactor(history): narrow change-callback exception surface --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
f36c3f6ae5 |
feat(text_injection): add RemoteDesktop portal injection backend (#885)
* feat(text_injection): add RemoteDesktop portal injection backend Wayland offers exactly one sanctioned key-injection path: org.freedesktop.portal.RemoteDesktop's NotifyKeyboardKeysym. It reaches native clients on compositors with no input-method-v2 bridge, works from inside the Flatpak sandbox without /dev/uinput or a helper daemon, and delivers keysyms so non-US layouts survive -- unlike ydotool's evdev keycodes. RemoteDesktopPortal is a synchronous client over a dedicated worker thread holding a private bus connection and GLib context, which keeps Request::Response delivery deterministic. Sessions are created lazily on first use so startup probing only reads the interface version; v2 portal sessions persist a restore token (mode 0600 under data_dir) so the permission prompt appears once. Backend selection now prefers the portal on Wayland: it is probed at runtime ahead of ydotool/wtype/xdotool in autodetection, is pinnable via VOCALINUX_FORCE_BACKEND=portal or text_injection.backend, and demotes to the previous order on runtime failure. Flatpak still needs --device=all for evdev hotkeys, but uinput is only a fallback path now. Closes #750 * fix(text_injection): resolve Greptile findings on RemoteDesktop portal injection - Move persist_mode/restore_token to SelectDevices options (portal v2 sends them there, not CreateSession); a bad token is ignored rather than sinking the session, so drop the stale-token retry paths that could issue a second Start after a user cancel - Track delivered units on RemoteDesktopPortalError so Wayland fallbacks retry only the undelivered tail instead of retyping the prefix (text chars, backspace taps, shortcut steps) - Give queued portal jobs a single deadline budget across CreateSession/SelectDevices/Start waits; on submit timeout the job is cancelled via a Gio.Cancellable plus abort checkpoints so the worker cannot keep emitting keys after the caller gave up - Close sessions whose Start never succeeded - Release held shortcut modifiers on mid-step failure - Skip the portal probe when an installed wtype/ydotool backend is explicitly pinned - Type annotations on the new signatures and touched tests --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
24fddc4344 |
chore(release): add snap-promote workflow for gated stable releases (#881)
* chore(release): add snap-promote workflow for gated stable releases Issue #783 option 2 (safer): v* tags keep auto-publishing the snap to edge + candidate in release.yml, and latest/stable is promoted by a manual snap-promote.yml dispatch after candidate QA. The dispatch resolves the revision on latest/candidate and refuses to promote unless its version matches the tag, so a typo or stale candidate fails the run instead of shipping the wrong revision. Declaring the snap-stable environment with required reviewers adds an approval gate on top of the dispatch. Fixes #783 * fix(ci): serialize snap promotions and re-verify candidate awk used the gawk-only \S which mawk reads literally, so the latest-track candidate row never matched. The promote step also trusted a revision resolved minutes earlier, letting an overlapping promotion downgrade stable. Serializes dispatches and re-resolves the candidate right before release. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
5e531cbac0 |
refactor(audio): extract capture source from recognition manager (#883)
* refactor(audio): extract capture source from recognition manager Move audio capture out of the silence-segmented dictation loop into vocalinux.audio.capture: a reusable PortAudioCaptureSource that owns device resolution, format negotiation, downmixing, resampling, and stream lifecycle (open/read/reopen/close). The dictation loop now consumes 16 kHz mono chunks and keeps only segmentation policy (VAD, silence counters, buffer limits, reconnection backoff). This unblocks alternate capture sources (PipeWire system-audio capture for #751, tdrz long-form output for #756) which need the same capture mechanics without dictation's silence segmentation. Behavior is identical: the same helpers run in the same order, moved verbatim. They are re-exported from recognition_manager for existing imports, and a CaptureSource protocol is declared in common_types.py. Tests repoint their patches of the moved helpers to the new module. Closes #760 * fix(audio): annotate capture API and keep a superseded session's source * fix(audio): reset retry counters when a new capture session starts * fix(audio): keep last-error timestamp across capture sessions The new-session reset wiped _last_audio_error_time, so an error within five seconds of the previous session's error always took the reconnect path instead of failing fast. Only the attempt count is per-session; the timestamp is also the rapid-failure throttle. --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
9e6a03cd20 |
feat(settings): make the settings category list scrollable (#886)
* feat(settings): make the settings category list scrollable Wrap the sidebar category ListBox in a Gtk.ScrolledWindow (matching the page wrappers) so the dialog can shrink vertically without clipping categories and new pages do not pin the minimum window height. Fixes #678 * fix(settings): reveal the selected sidebar row in the scroller * fix(settings): retry sidebar scroll until the selected row is laid out --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
ff108595fd |
fix(hotkeys): consume the dictation shortcut so it stops leaking to apps (#873)
* fix(hotkeys): consume the dictation shortcut so it stops leaking to apps Grab each monitored keyboard with EVIOCGRAB and re-emit every event the shortcut does not own through a paired uinput clone. The configured shortcut key is now consumed by Vocalinux instead of also reaching the focused app (right-alt opening menus in Thunderbird/Firefox, F11/F12 triggering browser actions). - Pure-modifier gestures reserve the modifier's events entirely; bare function-key shortcuts consume that key; modifier+key combos consume the main key only while the combo owns it, so typing and chords like Alt+Tab keep working and injected text is untouched - BUS_VIRTUAL devices (our clones, ydotool, remappers) are never grabbed - EV_REP is stripped from clones and repeats are forwarded verbatim, so repeat rate is preserved without doubling - Feedback event types (EV_LED/EV_SND/EV_FF) are never re-injected - Falls back to ungrabbed listening when /dev/uinput or the grab is unavailable, warning once that keys will also reach the app Fixes #871 * test(hotkeys): cover grab/forward edge cases Race cleanup on duplicate open, forwarder close errors during stop and device removal, mid-SYN_DROPPED forwarding, write failures, and the combo consume edge cases (unowned repeat/release, missing main key). * fix(installer): grant read on /dev/uinput so key suppression works The evdev backend's uinput clone opens /dev/uinput O_RDWR, but the installed 80-dotool.rules gave the input group write-only access (0620), so shortcut suppression silently fell back to ungrabbed monitoring on stock installs. Bump the rule to 0660 and refresh it when the installed copy differs so existing installs pick up the fix. * fix(hotkeys): harden evdev grab/forward lifecycle and udev install - release the device grab and close the clone when a uinput write fails - close all devices if the monitor exits unexpectedly (releases grabs) - monitor virtual devices too; skip only our own uinput clones - track consumed combo presses per device so releases stay paired - after SYN_DROPPED, release keys the clone still believes are held - append the uinput udev rule instead of overwriting local rules - annotate new signatures and narrow the clone-creation catch * fix(hotkeys): reconcile swallowed combo releases after SYN_DROPPED - resync the swallowed-press pairing even when the clone holds no keys; a dropped combo release otherwise leaves the next ordinary press consumed, eating a keystroke - narrow every new close-cleanup catch to OSError/IOError/RuntimeError with debug context instead of blanket Exception passes --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
004696bbe0 |
refactor(settings): consolidate dialog guard flags and widget construction (#878)
* refactor(settings): consolidate dialog guard flags and widget construction The settings dialog grew the same work in several places. This gathers the shared pieces into module helpers with no behavior change: - _suppressed(): a context manager for the try/finally flag dance the handlers used to open-code for _populating_models, _processing_language_change, _simple_syncing, and _simple_driving. The apply-guard handoff and the nested resync keep their explicit save/restore because they do not fit a lexical block. - _handlers_suppressed(): one name for the "initializing or applying" check every handler repeated on entry. - _set_no_show_all_visible(): the clear-flag / show / re-arm sequence for rows created hidden, used by the second-language row and the custom-shortcut row. - _add_switch_row(), _combo_chrome(), _new_spin(), _connect_picker_entry(): the repeated switch-row, tooltip + scroll-guard, spin-button, and picker-entry wiring. - _undownloaded_model_info() and _make_download_dialog(): the per-engine download gate both apply paths carried verbatim. Flags stay plain booleans and helpers stay module-level so existing tests keep working unchanged. * fix(settings): annotate download dialog builder and align visibility docs --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
ba721779c3 |
fix(downloads): abort blocked model fetches on cancel (#888)
* fix(downloads): abort blocked model fetches on cancel requests.get() has no way to abort while it is still resolving, connecting, or waiting for response headers — which is what an unreachable download server looks like. The cancel flag was only checked between response chunks, so Cancel sat until the request timed out. The blocking open now runs on a helper thread the caller polls every 200 ms; on cancel it raises at once while the abandoned attempt stays bounded by the existing timeouts. The settings dialog's download workers also reported "Model ready to use" whenever the apply returned True, but reconfigure() can no-op when the selection matches live state — success without a download. They now pass force_reinit so a reported success means the engine actually initialized the downloaded model. Fixes #679 * fix(downloads): bound cancel openers, close late responses - Track blocked request opener threads and give each attempt its own stop event so a cancel-and-retry cannot stack unaccounted requests and a response landing after cancel is closed instead of leaked - Catch requests.exceptions.RequestException in the opener instead of a bare Exception - Close the streaming response on the mid-stream cancel path - Make the blocked-open test helper cancellable and reap tracked openers; check each download worker call site for force_reinit separately; add missing return annotations on new test helpers and methods --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
c9ea6c7de7 |
feat(flatpak): publish a signed self-hosted OSTree remote on releases (#875)
* feat(flatpak): publish a signed self-hosted OSTree remote on releases (#785) release.yml gains publish-flatpak-remote: it imports the per-arch .flatpak bundles into an OSTree repo, signs commits and the summary with a provisioned GPG key, regenerates static deltas and the vocahq.flatpakrepo/index page, and pushes to VocaHQ/vocalinux-flatpak served by GitHub Pages — the Homebrew-tap analogue, so installs auto-update via flatpak update instead of per-release downloads. The job is secret-gated like publish-aur (FLATPAK_GPG_PRIVATE_KEY + FLATPAK_REPO_TOKEN) and skips itself while unconfigured. Prior repo objects are pulled back in from gh-pages so clients get incremental deltas. The manifest now declares branch: stable and the flatpakrepo declares DefaultBranch=stable + RuntimeRepo for the Flathub GNOME runtime. Docs: new docs/FLATPAK_REMOTE.md (one-time setup, rotation, verification); README, docs/INSTALL.md, packaging/flatpak/README.md and the release notes now lead with the remote and keep release bundles as the no-auto-update fallback. tests/test_release_integrity.py pins the job's shape. Closes #785 * fix(flatpak-remote): publish a .flatpakref and gate the remote to stable tags - needs build-and-release so the remote cannot lead the GitHub Release - stable tags only: prereleases must not reach `flatpak update` - probe gh-pages before the empty-repo fallback: a transient checkout failure no longer discards remote history - publish com.vocalinux.Vocalinux.flatpakref (RuntimeRepo lives there, not on .flatpakrepo) plus vocahq.gpg, so clean installs resolve the GNOME runtime and key rotation works via remote-modify --gpg-import - gate the release-notes remote instructions on the publish actually running; the bundle path stays primary otherwise - document rotation via remote-modify --gpg-import / remote-delete --force and the explicit Flathub setup step --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
d29c884782 |
fix(settings): gray out Dictation Tone while sound effects are off (#877)
* fix(settings): gray out Dictation Tone while sound effects are off The Enable Sound Effects switch mutes every dictation cue, so the tone picker changed nothing while it was off. The row now goes insensitive with the switch and follows the state loaded from config, matching the keep-alive timeout and auto-pause sensitivity pattern. Also bump the pre-commit isort pin 5.13.2 -> 8.0.1 to match uv.lock: the two versions disagree on single-name import wrapping, so the old hook collapsed imports the locked isort (and CI) then expanded back. Fixes #849 * test(settings): cover tone-row sensitivity on settings load --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
77def71354 |
fix(audio): stop passing explicit index when zero devices enumerate (#891)
* fix(audio): stop passing explicit index when zero devices enumerate With zero audio devices, _resolve_valid_input_device returned the saved preferred_index, so _open_capture_stream opened input_device_index=<stale> — a device that cannot exist — and Pa_OpenStream aborted the whole process (SIGSEGV) before the clean IOError->ERROR path could run. Zero enumerated devices now returns None so PyAudio falls back to the system default and fails catchably; a negative count (enumeration failure) still passes the preferred index through. * test(audio): cover capture path omitting device index * test(audio): cover saved-device zero-enumeration capture path * test(audio): annotate new resolver tests |
||
|
|
a89966a744 |
feat(recognition): record while an idle model reloads (#851)
* feat(recognition): buffer audio during model reload * fix(recognition): recover buffered reload failures * test(recognition): cover buffered reload edge cases * fix(recognition): narrow buffered reload errors * fix(settings): wait for IDLE before restoring Test Dictation callbacks Buffered reload's stop_recognition() returns while PROCESSING. Test Dictation must keep its text callbacks until recognition reaches IDLE so the test utterance cannot inject into the focused app. * style: apply Black formatting to PR 851 files * fix(settings): cancel buffered reload before Test Dictation timeout restore On the ~3min idle-wait timeout, do not restore live injector callbacks while a buffered reload worker may still deliver text. Cancel via _cancel_reload_recording off the GTK thread, then restore; if cancel is unavailable or fails, leave test callbacks installed and show an error. Add return/param annotations and regression coverage. * fix(settings): keep Test Dictation waiting if buffered cancel fails After the idle-wait timeout, if cancel is unavailable or raises, keep polling with test callbacks and _test_active instead of marking the test done. Ending early left the test callback as the only text destination, so later normal dictation could land in the hidden test buffer. Restore live injectors only once recognition reaches IDLE. * fix(settings): reset Test Dictation timeout-cancel flag per session Clear _test_timeout_cancel_attempted when a test starts, when entering the buffered idle-wait, and when restoring UI so a later Test Dictation can still cancel after timeout. Narrow the cancel-worker except clause to AttributeError/OSError/RuntimeError/TypeError so unrelated defects are not treated as ordinary cancel failure. * fix(recognition): signal capture completion when buffered capture dies A dead microphone during a buffered model reload left should_record=false with no stop_recognition handoff, so the reload worker blocked forever on _capture_finished.wait(). Signal the event from the audio thread's failure teardown paths instead. Refs VocaHQ/vocalinux#851 --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> |
||
|
|
db07590761 |
feat(activation): D-Bus activation for compositor global shortcuts (#568)
Rebased onto latest main (conflict-resolution squash for PR #568). Co-authored-by: Alexander <ab@digital-benefits.de> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
c0217a8aec |
refactor(installer): generate distro package map (#872)
* refactor(installer): generate distro package map * fix(installer): handle Debian derivative package maps * docs(installer): document generated package map * fix(installer): validate package map and refresh derivative probes * fix(installer): name openSUSE package roles |