Commit Graph
96 Commits
Author SHA1 Message Date
Web-BenefitsGitHubAlexanderDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.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>
2026-09-29 21:19:29 -07:00
Sergey SilaevandGitHub 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
2026-09-29 21:16:03 -07:00
Jatin K MalikandGitHub fb9166ed9e feat(settings): run local VocaGateway from Advanced (podman-first) (#774)
* feat(settings): run local VocaGateway from Speech Engine (podman-first)

Add an optional Local VocaGateway panel next to Remote Server so Linux users
can start the pinned v0.1.0 compose project with podman (docker fallback),
watch Live/Pairable/Ready status, pair a phone without loopback QR URLs, and
preset remote_api to the gateway transcriptions endpoint. Stop preserves
volumes; Flatpak fails closed; tray offers Stop only when this session manages
the gateway. Default on-device engine stays whisper.cpp.

* fix(gateway-embed): drop nonexistent compose cpu profile

VocaGateway v0.1.0 exposes default service gateway plus native/cuda/vulkan
profiles, not cpu. Start and stop the default service without --profile, keep
the latest image pin rejection, and prefer save_config for the remote_api preset.

* fix(gateway-embed): keep compose --profile cpu per embed contract

v0.1.0 has no cpu profile entry, but Compose still starts the unprofiled
gateway service when --profile cpu is set, and keeps cuda/vulkan/native off.
That matches the frozen desktop embed contract and stays forward-compatible.

* fix(gateway-embed): harden embed path for settings and compose

Keep podman/docker probing off the GTK thread, reject link-local and
loopback public URLs for phone QR, sanitize compose .env image overrides,
and cap bootstrap token reads so a hostile file cannot blow memory.

* fix(gateway-embed): keep LAN toggle and phone QR in sync

When Allow LAN flips while compose is already managed, rewrite publish
host and PUBLIC_URL and recreate the gateway before Pairable comes back.
Also reject default docker/podman bridge addresses for phone QR.

* test(gateway-embed): pin bridge and LAN-gate pairing cases

Cover 172.17.0.2 / link-local not becoming Pairable, and the mid-run
LAN toggle path that must wait for compose republish before advertising
a phone URL.

* style(gateway-embed): black format embed modules

* fix(gateway-embed): reject short-form loopback and localhost.* for QR

Align Pairable/QR host checks with vocagateway desktop-embed policy so
127.1 / localhost.* never become phone-reachable display URLs.

* fix(gateway-embed): detach Settings status listener on destroy

Gateway poll callbacks were left registered after Settings closed, so
idle_add could touch destroyed GTK widgets while compose stayed up.

* fix(gateway-embed): pass host_env to bridge ip probe

_bridge_iface_ips shells out to host ip; without host_env the AST
gate fails and AppImage bundles can hand the wrong GLib to ip.

* fix(packaging): ship gateway_embed in the AppImage

The explicit setuptools packages list omitted the new package, so the
bundle booted from source trees but died with ModuleNotFoundError on
every distro boot test.

* style(packaging): black the gateway_embed package guard

* fix(settings): pack local VocaGateway into the Advanced island

The rebase onto main auto-merged textually onto the simple Speech Model
page. Keep the gateway card on advanced_box, next to Remote Server.

* fix(gateway-embed): treat missing Compose as unavailable

When podman or docker is present but Compose is not, stop advertising Run.
Return empty compose_args, show an install-Compose hint, and cover it with a unit test.

Also add module loggers on the new gateway_embed modules and annotate the
gateway Settings/tray callbacks Greptile flagged. just lock found no lockfile drift.

* fix(gateway-embed): keep Allow LAN bind matched to the switch

Snapshot the LAN flag for start/republish, record what compose actually
got, then republish again if the switch moved mid-flight so 0.0.0.0 never
lingers while Allow LAN reads off.

* fix(gateway-embed): detect leftover compose after Quit

Quit does not stop compose, so a LAN-published gateway can keep running.
Settings and the tray treated that as Stopped because managed_by_us is
only process memory. Probe compose after runtime detection, show Stop
when it is actually up, and let Stop work even if this session did not
start it.

* fix(gateway-embed): restore LAN switch from leftover compose

Quit leaves compose running, so the next launch now reads
VOCAGATEWAY_PUBLISH_HOST from the embed .env, adopts the project as
managed, and keeps Allow LAN honest so republish can close 0.0.0.0.

* fix(gateway-embed): restore .env if LAN republish fails

compose recreate can fail after write_env_file already pointed
VOCAGATEWAY_PUBLISH_HOST at loopback. Restore the previous .env so
orphan adopt still sees a live 0.0.0.0 bind, and snap Allow LAN back
to the running publish mode.

* style(gateway-embed): type helper args and black MagicMock

* refactor(types): move Readable protocol to common_types

AGENTS.md keeps Protocol interfaces in common_types. Pairing now imports
the shared Readable surface instead of a local _Readable helper.

* fix(gateway-embed): satisfy mypy on rebased embed code

* fix(audio): keep duck restore point when a set reports failure
2026-09-29 15:33:27 -07:00
Jatin K MalikandGitHub eef2554f7c feat(audio): lower other audio while dictating (#861)
* feat(audio): lower other audio while dictating

Opt-in setting lowers the default PipeWire or PulseAudio sink while the
microphone is open, then restores that exact level. It stays off by default.

* fix(test): keep pytest.approx working when numpy is mocked

Other test modules replace sys.modules["numpy"] with a MagicMock at import
and leave it there. pytest.approx then crashes on isinstance(..., numpy.bool_).

* fix(audio): restore every channel and do not duck if it cannot be undone

A failed restore-point write no longer lowers the sink. PulseAudio puts
each channel back, and a dead microphone restores playback instead of
waiting for an explicit stop. The recognition test patches the real
audio module when an older test left a mock behind.

* fix(test): put the audio-feedback mock back after the ducking test

The recognition test has to patch the real module, but later tests still
expect the mock that test_text_injector installs. Restore that mock when
the test ends, and annotate the new helpers.

* fix(test): annotate the playback-duck tests

AGENTS.md requires a type hint on every new function. These tests were the
ones still missing them.

* fix(audio): skip arming playback duck after recording already failed

* chore: retrigger review after rebase
2026-09-29 14:24:28 -07:00
HashingandGitHub 12e440ce1c feat(text-injection): let config.json pin the injection backend (#649)
* feat(text-injection): let config.json pin the injection backend (#476)

Autodetection has to infer whether IBus commits actually reach the
focused application, and it cannot verify that: commit_text() reports
success even when the compositor drops the text. When the inference is
wrong the failure is silent, and dictation appears to work in XWayland
windows while doing nothing in native ones.

VOCALINUX_FORCE_BACKEND already provides an escape hatch, but it is an
environment variable: it covers a single run, is undocumented outside
the test suite, and a user whose compositor is misdetected has to wrap
the launcher in a shell script to make the choice stick.

Add a persistent `text_injection.backend` setting alongside it, accepting
the same ibus/wtype/ydotool/auto values. The environment variable still
wins so a backend can be A/B-tested for one run without touching the
saved configuration, and the startup log now names which of the two
pinned the choice so a stale setting is diagnosable from --debug output.

Reads the file directly rather than through ConfigManager, keeping this
package independent of the UI layer, exactly as _should_copy_to_clipboard
already does. An unreadable or corrupt config falls back to autodetection
instead of blocking startup, and an unrecognised value is ignored with a
warning so a typo cannot silently pin the wrong backend.

Defaults to "auto", so existing installs behave exactly as before.

* fix(text-injection): tell an explicit VOCALINUX_FORCE_BACKEND=auto from unset

The variable is meant to override the saved text_injection.backend pin for a
single run, which is what makes the two paths A/B-testable without editing
config. Setting it to auto -- the value that means "autodetect this time" --
did not do that: the reader collapsed unset and an explicit auto into the same
"auto" return, so the caller read both as "nothing was forced" and fell through
to the saved pin. The pin won, and there was no way to ask for autodetection
for one run short of editing the config file the variable exists to avoid.

Read the variable as three states instead: unset, an explicit auto, or a
backend name. Only the unset state consults config, so an explicit auto now
short-circuits to autodetection.

An unrecognised value is deliberately treated as unset rather than as auto. A
typo means the user failed to override their preference, not that they asked to
discard it, so falling through to a valid saved pin is the kinder reading.

_forced_backend() keeps its signature and its collapsed return, so its existing
callers and tests are untouched; the new three-state reading lives alongside it
and is what the preference resolver consumes.

* feat(text-injection): accept an xdotool pin and warn when ibus overrides the denylist

Two gaps in the pinnable backend set.

xdotool was not accepted, even though on X11 it is the tool that actually does
the typing. Users were told to pin a *Wayland* backend to mean "not IBus", and
get xdotool as a side effect. Naming it directly is what they were reaching for.
On Wayland it needs no special handling: the value skips IBus like any other
non-ibus pin and falls through to the existing XWayland fallback, so no branch
is added to the selection chain.

The accepted values now live in one tuple, because four places listed them --
both readers and both "expected ..." messages -- and a fifth value would have
had to be added to all four. The two messages derive from the tuple, so they
cannot fall behind it.

Pinning ibus also silently bypasses the compositor denylist. That check exists
because a denylisted compositor makes IBus report a commit as delivered while
the text never arrives (#478, #485), so a pin is the one setting that can put a
user back into a failure with no symptom other than nothing happening. The pin
is still honoured -- an override that ignores you is worse than one that warns
-- but it now says so at warning level, names the compositor, and gives the
remedy. It stays quiet when the compositor genuinely bridges IBus, including a
denylisted one running the ibus-wayland bridge, and on X11 where the denylist
does not apply.

The check is evaluated once. The guard chain short-circuits on the pin, so a
pinned run never reaches the existing compositor branch, and this is its first
and only call -- worth knowing because the check can shell out to pgrep or
gdbus. A test asserts the call count so that stays true.

Documents xdotool in the value table and rewrites the X11 paragraph, which
described xdotool as the implicit result of pinning something else rather than
as a value you can name.

* fix(text-injection): say so when the pinned backend is not the one in use

A pin could be quietly ignored. Pinning a tool that is not installed fell
through to autodetection and typed with whatever it found, under a log line
that had already announced the pin was overriding autodetection. Pinning
xdotool on Wayland did nothing at all, since the selection chain reaches
ydotool and wtype first. Pinning ibus where IBus support is unavailable skipped
the whole IBus branch. In each case the user set a preference, saw no
complaint, and got something else.

These are one condition, not three: whatever ended up doing the typing is not
what was pinned. The check compares the pin against the backend read back from
final state, so a cause nobody has thought of yet is covered without another
branch. It runs at the end of construction rather than inside
_check_dependencies, which returns early on two paths and which the wtype probe
can still override afterwards. The reason clause is added only where a generic
check answers it -- the binary is absent, or IBus is unavailable -- and omitted
otherwise rather than guessing.

Reporting the source separately was letting the environment variable and the
config setting disagree. _check_dependencies resolved the backend, then
re-derived which source had won by asking again -- so the environment was
parsed twice per construction, and a malformed value was reported to the user
twice. Resolution now returns the backend and its source together, which fixes
the duplicate warning and means the source named in a log line is the one that
actually chose the backend. _backend_preference() keeps its signature and reads
the first element.

Two log lines also claimed VOCALINUX_FORCE_BACKEND regardless of where the pin
came from, so a config-set backend was reported as an environment variable the
user had never set. They now name the real source.

The new warning is a startup diagnostic, not a live invariant: a tool switched
later by _try_recover_from_fallback is not reported again. Noted in the
docstring so the boundary is deliberate.

* docs(user-guide): file the injection-backend section under Troubleshooting

The section sat under Customization, directly beneath GPU Acceleration --
where the paragraph immediately above it reads "To check which backend is
being used" about Vulkan and CUDA. The word means something else there, and
two adjacent sections using it for different things is a bad place to explain
either.

It also is not a customization option. It exists because a compositor can make
IBus report a commit as delivered while nothing arrives (#478, #485), and
someone hitting that reads Troubleshooting, not Customization. The section
already opens on the symptom, so it needs no rewriting to belong there.

Pure reorder: no wording changes, and the heading text is unchanged so any
existing link still resolves.

* test(text-injection): cover the pin paths that skip the selection chain

_check_dependencies had three uncovered branches, all reachable by pinning a
backend and none of them exercised: the two early returns that hand over to
IBus before a tool is chosen, and the daemonless-ydotool fallback a wtype pin
lands on when wtype is absent. The early returns matter most, since the pin
check runs at the end of construction precisely so those paths cannot skip
it, and nothing proved that.

Adds the ydotool counterpart to the missing-binary case, so a check keyed on
one tool name cannot pass by coincidence; an X11 test for the guide's claim
that any non-ibus pin turns the IBus path off, with a control asserting IBus
is constructed without a pin so the assertion is the pin's doing; and an
end-to-end explicit-auto case, which until now was only proven at the
resolver rather than through selection.

The pin log line said it was "overriding backend autodetection" before
anything had been checked. An unavailable pin falls through to autodetection,
so that line announced an outcome it could not know and then read as
contradicting the "was not applied" warning. It now states the request only.

_run_with_pin gains ydotoold_ready, defaulting to the previous behaviour so
existing callers are unchanged.

* fix(text-injection): report a config file the backend pin cannot be read from

A corrupt config.json disappeared into logger.debug. The setting is
hand-edited -- that is the workflow the user guide documents -- so a stray
comma is a likely way to reach this, and staying quiet left the user with
autodetection and the silent IBus miss the pin was set to avoid, with nothing
to tell that apart from the pin not working. Unknown values already warned;
an unusable file now does too, naming the file and the parse error.

The bare handler was also catching AttributeError by accident. Valid JSON
that is not an object -- a bare list, string, number or null -- reaches the
lookup and raises there rather than at parse time, and so does a
text_injection section that is not an object, where the top level is a
mapping and the section is not. Both are now shape checks rather than
exception handling, which is what lets the handler narrow to OSError and
ValueError without those five shapes escaping it.

Locating the file moves out of the try. It fixes the handler being unable to
name the path it failed on, and it is a deliberate boundary: config_dir()
resolving a path is not a failure this should paper over, so it is no longer
guarded here at all.

A missing config.json stays silent, which is the default install.

* test(text-injection): stop the ibus-pin tests depending on an installed ydotoold

The bridged-compositor and X11 cases asserted that a run logs no warnings at
all. That is broader than what they are about, and _run_ibus_pin patches
shutil.which to claim every tool exists without patching the probe that then
shells out to one, so the real _ensure_ydotoold ran and warned whenever
/usr/bin/ydotoold was absent. The tests passed on a machine that happens to
have it installed and failed on the CI runners that do not.

The assertions now name the warning they are about, and the helper patches
_ensure_ydotoold the way its sibling _run_with_pin already did, so nothing in
this class of test reads the host's installed packages.

* fix(text-injection): report pending IBus initialization state

* docs(text-injection): clarify backend pin behavior

* fix(config): preserve a hand-edited backend across unrelated saves

ConfigManager writes its full in-memory configuration on every save. A user who edits text_injection.backend in config.json while Vocalinux is running therefore loses that choice when an unrelated Settings or tray save writes the stale default back.

Remember only the backend value observed at load or after a successful save. If the in-memory value is unchanged, preserve the current value from disk; if it changed through ConfigManager, treat that as the intentional write. Missing and explicit auto both reset to auto.

The save aborts rather than overwriting a file whose JSON or text_injection shape cannot be read safely. This is deliberately per-key: no recursive configuration merge or injection-layer dependency is added.

* style(text-injection): remove redundant typing suppression

* fix(config): keep invalid UTF-8 config from blocking startup

* fix(config): preserve malformed backend sections on unrelated saves

* fix(config): retain text setting changes after malformed config

* test(text-injection): annotate backend pin test helpers
2026-09-29 12:24:59 -07:00
7bdbedce0b feat(language): follow the active keyboard layout while dictating (#837)
* feat(language): follow the active keyboard layout while dictating

Adds a "Follow keyboard layout" switch to Settings -> Speech Model ->
Advanced. With it on, the active keyboard layout is re-read at the start
of every dictation and the matching language is used, so someone who
switches layouts to work in two languages gets the right one without
opening Settings.

#796 already seeded the language once on first run, but from the
*configured* primary layout, which never changes as the user switches
between them: a "fr,us" setup reports fr forever. detect_active_keyboard_
layout() reads GNOME's input sources instead. It takes mru-sources rather
than the current index, because that index goes stale -- observed still
reading 0 after a switch to the second layout -- which is why the IBus
injection path already trusts the MRU list (#497, #738).

An active source that is an IBus engine rather than an xkb layout is a
real answer that cannot be mapped, so the language is left to the engine's
own auto-detect. Borrowing the configured xkb layout there would pin
English for someone typing Japanese through mozc-jp.

The mode is a switch over the language picker rather than an entry inside
it, since it is a mode and not a language. While on, the picker is
disabled and shows what the layout resolves to; turning it off pins that.

A language change no longer re-initialises the engine when the loaded
model takes the language per utterance, which is the whole whisper
family, so following a layout costs a settings read rather than a model
reload. VOSK loads one model per language, Parakeet ignores the picker,
and an English-only model has no non-English weights, so Settings refuses
the mode for all three rather than offering a switch that does nothing.

Fixes #821

* fix(language): keep follow mode armed and map IBus engines

Swap English-only whisper.cpp weights to the multilingual sibling while
passing the layout sentinel into reconfigure, so follow mode stays armed.
Map common GNOME IBus engines onto catalogue languages instead of
leaving them unmapped.

* fix(language): stop IBus hyphen false-maps and gate the sibling swap

The last-token hyphen heuristic mapped mozc-us and foo-us to English.
Restrict it to *-jp/*-kr and name Mozc's extra engines as Japanese.
Only whisper.cpp may consult the whisper.cpp download catalog.

* fix(language): keep multilingual weights while following layout

The picker shows the active layout while follow is on, often en-us.
Deriving whisper.cpp .en from that display made Settings refuse the
mode as English-only, then the next auto-apply persisted a concrete
language and disarmed follow. Use LANGUAGE_FOLLOWS_LAYOUT for .en vs
multilingual derivation instead, including on the load path before
the switch is synced.

* fix(language): type follow helpers and log sibling-swap failures

* fix(test): stub follow-layout model rec so mocked psutil does not blow up

follow-mode English-only guard calls get_recommended_whispercpp_model, which
reads RAM through psutil. Full CI leaves a MagicMock in sys.modules["psutil"],
so ram_gb >= 16 raises TypeError. Patch the settings_dialog alias to return a
multilingual tiny rec so the assertion still holds without real RAM.

* fix(language): catch specific errors on layout lookup and sibling swap

* fix(language): swallow more sibling-swap reconfigure failures

ImportError, TypeError, AttributeError, and ChecksumError from the
multilingual swap must not abort dictation or disarm follow mode.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-29 01:06:32 -07:00
Kanishk PachauriandGitHub f4e86f6325 fix(deps): clear open dependency advisories (#869) 2026-09-24 04:42:08 +05:30
Jatin K MalikandGitHub 5b4e2c3572 fix(snap): add hardware-observe so hotkeys can read input devices (#858)
Snap confinement denies /proc/bus/input/devices unless hardware-observe
is connected. Declare the plug, tell users to connect it next to
raw-input, and update the in-app hints.

Fixes #857
2026-09-23 00:09:54 -07:00
acd90fdd05 fix(installer): install hash-pinned dependencies and build tools (#856)
* fix(installer): install hash-pinned dependencies and build tools

* fix(installer): add cairo-devel for the pinned pycairo build

* fix(installer): reject missing or empty bootstrap exports

* fix(installer): preserve remote behavior for older tags

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-23 00:09:48 -07:00
Jatin K MalikandGitHub 1f121546fb fix(release): attach the snap to GitHub Releases (#839)
* fix(release): attach the snap to GitHub Releases

v0.17.0 packed the snap, then Store review of uinput failed the job, so
the file never reached the tag. Build and attach first. A Store hold is
a warning, not a failed release. SHA256SUMS covers the .snap.

snap-backfill.yml rebuilds an existing tag, appends the snap to the
manifest, and attests only that file.

* fix(release): keep snap tags out of the shell and fail real Store errors

Pass the tag through the environment like verify-release.yml. Treat only
an allow-installation review as a successful Store publish. snap-backfill
runs verify_release.py after it rewrites SHA256SUMS.

* style(test): format snap-backfill shell-injection guard with black
2026-09-16 19:40:13 -07:00
Jatin K MalikandGitHub 26f90b6998 chore(release): prepare v0.17.0
Minor on the stable line. Faster Whisper and Parakeet extras, Speech Model simple setup, Snap uinput, Flatpak bundles on the tag, and QA fixes from the release branch.

Fixes tracked in the PR. Post-release follow-ups: #834.
2026-09-16 00:18:09 -07:00
Jatin K MalikandGitHub 4b6e81d441 fix(snap): type into native Wayland apps with ydotool (#823)
* fix(snap): inject into native Wayland apps via ydotool

Edge rev 7 only ships wtype and xdotool. GNOME rejects the virtual
keyboard protocol, so the snap falls back to xdotool and types only
into XWayland windows. Native GTK/Qt apps get nothing.

Stage ydotool, request the uinput plug, and prefer ydotool when wtype
is rejected. Distro ydotool 0.1.x still needs a writable /dev/uinput;
log `snap connect vocalinux:uinput` when the Snap cannot open it.

* docs(snap): v0.16.2 edge has no uinput plug

The store snap (0.16.2, latest/edge rev 7) does not declare uinput.
`sudo snap connect vocalinux:uinput` fails with "has no plug named
uinput" and dictation only reaches XWayland apps. Spell that out in
README, INSTALL, UPDATE, the website changelog, and the store copy so
people do not run the 0.17 connect command against rev 7.

* docs(snap): drop the unconfirmed 0.17 version from uinput notes

Greptile P2: the tree is still 0.16.2 and the release process does not
name the next tag. Tell people to refresh until `snap info` lists the
uinput plug, instead of waiting on a version we have not cut.
2026-09-12 12:58:35 -07:00
Jatin K MalikandGitHub f48bdda525 docs: improve project documentation for a clearer public surface (#634)
* docs: polish project documentation for a clearer public surface

Rewrite the README and supporting docs for a more professional first
impression: less emoji and badge noise, install-first structure, and a
real documentation index. Add a Contributor Covenant, tighten SECURITY
and GitHub templates, and make distro/update guides read as product docs
instead of internal status notes.

* docs: split install guide and add changelog plus support pages

Break INSTALL.md into a short primary guide, manual/PyPI package lists,
and troubleshooting. Add root CHANGELOG.md and SUPPORT.md, wire them
into the docs index and README, and drop the outdated screenshot lag note
now that v0.15 assets are on main.

* docs: humanize PR copy and remove em dashes

Strip em/en dashes from the documentation rewritten in this PR, fix
mid-sentence colon fallout from that pass, and smooth a few stiff
phrases so the public docs read more natural.

* docs: list Faster Whisper after rebase onto main

Installer and CLI already accept faster_whisper. Document it next to
the other engines, drop the outdated screenshot lag note, and keep the
on-device wording honest for local vs remote.
2026-09-07 22:51:14 -07:00
7664266823 feat(commands): localize punctuation voice commands (#640) (#642)
* feat(commands): localize punctuation voice commands by language

Voice command phrases were English-only even when recognition language
was set to Italian, French, etc. Merge locale punctuation/line-break
aliases from the recognition language into CommandProcessor, keep
English phrases available, and document the behavior.

Fixes #640

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

* fix(commands): keep line breaks and French apostrophe aliases

Attach-left punctuation now eats only horizontal whitespace, so a
localized "new line" followed by a period or comma still inserts a
newline. French aliases include ASCII, U+2019, and backtick apostrophes,
and longest-match still wins so "point" cannot fire inside
"point d'interrogation".

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
2026-09-07 20:47:12 -07:00
Jatin K MalikandGitHub 2c6acbc15e fix(inject): layout-aware clipboard paste for Neo and friends (#788)
* fix(inject): resolve paste chord for non-QWERTY layouts

ydotool was sending physical KEY_V (47) for Ctrl+V. On German Neo that
key types p, so paste opened Print instead. Prefer wtype keysyms when
usable; otherwise resolve the evdev key for Latin v via xkbcommon, and
read KDE kxkbrc so Plasma Wayland Neo users are covered.

Fixes #787

* fix(inject): read Plasma active layout from real session sources

kxkbrc has no CurrentLayout/LayoutIndex, so multi-layout Neo always
fell back to us at index 0 and paste still hit KEY_V. Prefer D-Bus
getLayout or layout_memory.xml for the active layout, keep kxkbrc
for the list only, and prefer KDE over leftover GNOME on Plasma.
2026-09-07 18:10:43 -07:00
46c7914639 feat(ci): verify the published release, not just the workflow (#791)
* feat(ci): verify the published release, not just the workflow

* fix(ci): filter attestations by predicate, name a missing distribution

Two findings from review on #791.

The attestations query accepted any predicate, so an SBOM would have read as
build provenance while `gh attestation verify`, the command the notes hand
users, rejected the artifact. It now sends the same
`predicate_type=https://slsa.dev/provenance/v1`. Checked on a real digest: the
SLSA filter returns the bundle, a foreign predicate returns 404.

check_pypi compared whatever distributions were left, so a release that lost its
sdist passed on the wheel alone. Each expected suffix is now named when absent.
The lazy urllib import moved past the early return, where it belonged.

The other two findings do not hold. `workflow_run.head_branch` carries the tag
for a tag-triggered run: the last four release runs report v0.16.2, v0.16.1,
v0.16.0 and v0.15.0. And `host_env()` is for the app spawning host binaries out
of an AppImage; `scripts/` is in no `packages` entry and the AppImage build
never references it, so this cannot run from a bundle.

* fix(ci): read release digests from the REST API

gh release view --json assets omits digest on gh 2.46, which is what
many distros ship and what just verify-release often runs, so every
asset failed as undigested. Pull the release through gh api instead,
treat a null body as notes FAIL, and prefer removeprefix for the tag.

* fix(ci): fail verify when PyPI has an extra distribution

Matching release digests alone left a differently named wheel or
sdist on PyPI unchecked. Reject those extras too, same suffixes.

* fix(test): clear tempfile mock before urllib import

Other suites leave a MagicMock in sys.modules['tempfile'], which
breaks urllib.request import with a metaclass conflict in the extra
PyPI distribution check. Drop the stub (and stale urllib modules)
first, and assert the full problem list.

* fix(ci): restore real tempfile before urllib in verify test

The MagicMock tempfile left by recognition_manager still broke
urllib.error on CI. Put the real module back and drop cached urllib
packages before the import.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-09-06 23:01:56 -07:00
Jatin K MalikandGitHub 50ea1109d7 feat(snap): Snap packaging recipe and Snap Store publish strategy (#519)
* feat(snap): add Snap packaging recipe and store publish strategy

Add a strict-confinement snapcraft.yaml aligned with the current app
version and Flatpak sandbox lessons (mic, desktop, network models,
xdotool/xsel injection). Document human-only Snap Store steps and mark
install docs as not published yet.

Refs #48

* fix(snap): adopt-info so craftctl can set version from version.py

snapcraft pack failed: craftctl set version requires adopt-info on the part.

* fix(snap): stop staging GTK/GLib over the gnome platform

Prepending snap-staged GI typelibs and lib paths caused a segfault on
from gi.repository import Gtk. Use platform AppIndicator/GTK and only
stage PortAudio plus xdotool/clipboard helpers.

* docs(snap): note platform GTK path to avoid GI segfault

* fix(snap): route ALSA through Pulse so Settings does not abort

PortAudio enumerates ALSA devices when Settings opens. Missing
/usr/share/alsa/alsa.conf inside the snap triggered a PortAudio assert.
Ship an asound.conf that uses the pulse plugin, stage libasound2-plugins,
and bind ALSA paths via layout.

* fix(snap): keep ALSA conf under snap/local for snapcraft

Move asound.conf out of snap/alsa so snapcraft stops warning about
non-snapcraft paths in the snap directory.

* fix(snap): request raw-input for global keyboard shortcuts

Strict confinement blocks /proc/bus/input and /dev/input, so Wayland
evdev hotkeys never start. Declare the raw-input plug (manual connect)
and print snap-specific connect instructions instead of host input-group
hints.

* fix(hotkeys): discover keyboards via evdev when /proc is blocked

Snap raw-input grants /dev/input/event* but still denies
/proc/bus/input/devices, so find_keyboard_devices() returned empty
and hotkeys never started. Fall back to evdev.list_devices() when
/proc cannot be read.

* fix(snap): restore UI sounds and prefer wtype for Wayland inject

Stage paplay (pulseaudio-utils) and wtype; teach audio_feedback about
pw-play/canberra. xdotool-only injection often hits the wrong window on
native Wayland even when it reports success.

* fix(snap): sync 0.16.2/AGPL metadata and AppImage-safe audio env

Align snapcraft fallback version and store metadata with current main
(VocaHQ URLs, AGPL-3.0), keep docs pointing at #48 on VocaHQ, pass
host_env to pw-play/canberra paths, and document ordered Snap Store
human deploy steps.

* test(snap): harden audio host_env unit test against suite pollution

Patch os.path.exists like sibling audio_feedback tests instead of relying
on a real tempfile path that can collide with other suite mocks.

* style(snap): black-format new audio and snap packaging tests

* docs(snap): treat Store listing as live; edge→candidate→stable refresh

Name vocalinux is already on snapcraft.io (edge @ 0.14.0-beta rev 6).
Document upload/release without re-register, add snap-store-refresh.sh
helper, and point INSTALL/README/DISTRO at the live listing.

* fix(snap): move to core24 and close evdev capability FDs

core22 only ships Python 3.10, which fails requires-python >=3.11, so the
snap base is now core24 (Python 3.12) with the gnome-46 platform and the
Ubuntu 24.04 libasound2t64 package. Also close InputDevice after failed
capability scans and annotate the proc parser with TextIO.

* Snap: align store listing metadata and icon

Match snapcraft.yaml summary, description, license, website, and store
icon to the live Snap Store listing.

* Snap: trim packaging docs, script, and unused SVG

Drop store refresh wrapper, long packaging README, duplicate snap/gui SVG, and speculative pw-play/canberra paths; keep recipe and confinement fixes.

* ci: publish snap to edge and candidate on release tags

* Snap: set grade stable for candidate channel CI

* Snap: skip pip PyGObject; use gnome extension GI
2026-09-05 18:55:37 -07:00
Jatin K MalikandGitHub bb2b098193 chore(release): prepare v0.16.2 (#782)
Bump version metadata and series-aware notes for the 0.16.2 patch:
KDE leftover IBus, Wayland/IBus shortcuts, BackSpace delete that,
installer glslc, nightly stamp, release integrity, and CI/docs gates.
2026-09-05 15:04:48 -07:00
SergeyandGitHub a46000e249 chore(ci): test the distros the docs promise, and fix the docs that drifted (#773) 2026-09-04 23:28:58 -07:00
SergeyandGitHub e61133e246 feat(ci): gate AUR PKGBUILD builds on every PR (#772)
* feat(ci): gate AUR PKGBUILD builds on every PR

Nothing ever ran makepkg on the PKGBUILD before a tag pushed it to AUR
users, and the channel broke twice that way (#736, #757) — a PKGBUILD
change did not even start a CI job, because no paths filter matched
packaging/aur/**.

The gate (packaging/aur/build-test.sh, run in archlinux:latest — the
AUR is a rolling channel, so the image stays deliberately unpinned):

- swaps the tag-archive source= for a git archive of the checkout under
  the same filename and prefix, so build()/package() run unchanged and
  the gate answers "does this commit build on Arch"
- installs depends/makedepends from the repos; python-pywhispercpp
  (virtual, provided by the AUR backends, #579) and python-pynput (AUR)
  are satisfied by a stub so makepkg still verifies the full depends
  set, and pip-installed from the hash-pinned requirements/runtime.txt
  for the import smoke
- runs namcap on the PKGBUILD and the built package, failing on errors
  only (its warnings are noise around lazy imports and virtual depends)
- installs the package and imports the whole UI/recognition stack, plus
  the vocalinux --version entry point

Part of #701 (phase 6).

* fix(ci): stop partial-upgrading in the AUR gate and correct the #736 attribution
2026-09-04 23:25:09 -07:00
SergeyandGitHub 0ba029b015 chore(release): make a release verifiable, and pin what builds it (epic #701, phase 5) (#759)
* chore(release): build once, and publish sha256sums and provenance (epic #701, phase 5)

* chore(ci): pin every third-party action by commit sha (epic #701, phase 5)

* test(ci): anchor the dependabot guard on the ecosystem it must cover
2026-08-31 14:32:20 -07:00
Jatin K MalikandGitHub 21b8ee8d05 fix(aur): build on Arch setuptools 84 and skip 1.4 context_params (#757)
AUR --no-isolation uses extra python-setuptools 84, so the <82 cap blocked the 0.16.0 rebuild. AUR pywhispercpp backends are still 1.4 and crash if we pass context_params; skip that argument below 1.5.
2026-08-30 01:12:48 -07:00
Jatin K MalikandGitHub 68b00bd7d8 chore(release): prepare v0.16.1 (#755)
Bump the stable line from 0.16.0 to 0.16.1. README and UPDATE keep the 0.16 series highlights and add the patch-only fixes. Website changelog, schema, AppStream, and AUR PKGBUILD follow.
2026-08-30 00:49:09 -07:00
SergeyandGitHub 65381b4582 fix(appimage): build against a glibc floor users actually have (#743)
* build(appimage): pin the interpreter, packages and tooling

* ci(appimage): build inside a pinned ubuntu 22.04 image

* test(appimage): guard the pins and the base image

* docs: state the AppImage glibc floor

* fix(appimage): bundle libibus with its typelib

* ci(appimage): let the container run the isolated GI smoke test

* fix(appimage): keep the bundle's libraries out of host binaries

* fix(appimage): bundle libgpg-error with the libgcrypt that needs it

* docs(agents): require host_env() for spawned binaries

* ci: run the AppImage jobs when its other inputs change

* fix(appimage): check every bundled typelib, not just the seeded ones

* fix(ibus): keep the bundle when respawning our own interpreter

* fix(injection): pass host_env() to setxkbmap and notify-send

* test(ibus): wait for the IBus warmup thread before asserting
2026-08-29 14:46:50 -07:00
Jatin K MalikandGitHub 008fd3138e fix(injection): use Ctrl+Shift+V when pasting into terminals (#734)
* fix(injection): use Ctrl+Shift+V when pasting into terminals

Clipboard injection always sent Ctrl+V, which most terminal emulators
ignore. Detect standalone terminal windows and send Ctrl+Shift+V, with a
Settings override for nested IDE panels that window-class detection cannot see.

* fix(injection): fail closed when terminal window probe is mocked

Clipboard tests patch text_injector.subprocess.run, which replaces
stdlib subprocess.run. Window detection now ignores non-string probe
output so injection still uses Ctrl+V instead of raising TypeError.

* docs: fix overclaimed terminal auto-detect in USER_GUIDE

Auto-detect only runs on X11 and on Hyprland, Sway, and niri. GNOME and
KDE Wayland stay on Ctrl+V unless you set Clipboard Paste Shortcut to
Ctrl+Shift+V.
2026-08-27 00:12:27 -07:00
Sergey SilaevandGitHub 582d494633 chore(build): uv tooling, a Python 3.11 floor, and verified model downloads (epic #701, phases 2.5 & 5) (#713)
* chore(build): migrate justfile tooling to uv

All recipes now run through 'uv run' against .venv, with a shared
DEV_EXTRAS variable (dev + vad) because uv syncs the environment
exactly and would otherwise uninstall the dev tools between
recipes. 'deps' installs dev+vad; new 'deps-all' covers every
extra. 'build' switches to 'uv build'; 'version' uses --no-sync.

Bump [tool.uv] required-version to >=0.12,<0.13 for uv 0.12.x, and
raise the mypy python_version target to 3.12: mypy 1.19 in the lock
cannot target 3.9 and numpy 2.x stubs need a 3.12 target to parse
(mypy is not run in CI or pre-commit; this only affects 'just
typecheck').

* fix(tests): isolate test runs from real XDG state; fix Arch model-path crash

Local test runs failed en masse (185 failures) while CI stayed
green: tests picked up the developer's real ~/.local/share/vocalinux
models via the default XDG paths and switched IBus/keyboard code
paths through the desktop session env (XDG_CURRENT_DESKTOP/
XDG_SESSION_TYPE on GNOME/Wayland, issue #478 branches). conftest
now points XDG base dirs at a throwaway session-wide directory and
clears session variables at import time, mirroring a fresh CI
runner (paths like MODELS_DIR freeze at first import during
collection, so fixture-time isolation is too late).

Also guard the Arch branch of _get_system_model_paths(): it removed
/usr/local/share/vocalinux/models unconditionally and crashed with
ValueError on Arch hosts when a custom XDG_DATA_DIRS never added
that path. Add regression tests, and make the lock-file test assert
the actual $XDG_DATA_HOME/vocalinux contract instead of the
hardcoded ~/.local/share default.

* fix(tests): keep the IBus engine double a plain class

Python 3.14's unittest.mock re-instantiates a user subclass when it builds
child mocks (klass(name=...)). VocalinuxEngine inherits from whatever
IBus.Engine resolves to, and the test module bound that to the MagicMock
class itself, so every engine instantiation raised

    TypeError: VocalinuxEngine.__init__() got an unexpected keyword argument 'name'

on 3.14 while passing on 3.13. Replace it with a plain stand-in class that
still hands out per-instance MagicMocks for the IBus API surface the engine
touches.

The same pattern remains in test_ibus_engine.py, test_ibus_engine_utils.py,
test_ibus_active_engine_guard.py and test_ibus_xkb_wayland.py; those never
instantiate the engine, so they are unaffected for now.

* fix(install): build the venv from the system Python

install.sh looked up a bare `python3` from PATH. Started from an activated
virtualenv — a shell left in uv's .venv after `just deps` — it built venv/
from that interpreter, where distro PyGObject is invisible even with
--system-site-packages, and the run aborted with

    Distro PyGObject (python3-gi / python3-gobject) is not importable in the venv.

A mismatched venv/ then survived every retry, because the installer reuses
whatever venv it finds.

- drop an inherited VIRTUAL_ENV (and its bin from PATH) before any
  interpreter lookup
- select the interpreter explicitly: $SYSTEM_PYTHON (default
  /usr/bin/python3) or PATH python3, preferring one that can import gi,
  and create the venv with it instead of `python3`
- recreate venv/ when it was built by a different interpreter
- name the interpreter in the PyGObject guard's error

Documents the two environments (uv's .venv for tooling, venv/ for the
installed app) in CONTRIBUTING, the justfile header and AGENTS.md, and adds
the mismatch symptom to the INSTALL troubleshooting section.

* chore(python): raise the supported floor to 3.11

requires-python promised 3.9 while the CI matrix ran 3.9/3.10/3.11/3.13,
the classifiers advertised up to 3.14, mypy targeted 3.12 and install.sh
enforced 3.9 — five different answers to "which Python does this support".

Settle on 3.11 everywhere: pyproject (requires-python, classifiers, black
targets), install.sh's MIN_VERSION, check-system-deps.sh, the distro package
map, the CI matrix (now 3.11-3.14, matching the classifiers), the release and
nightly workflows, and the docs.

mypy now targets the floor instead of 3.12, so type checking covers what we
promise; numpy 2.x ships stubs in 3.12 syntax that a 3.11 target cannot
parse, so its imports are skipped. (mypy has 71 pre-existing errors either
way; no CI job gates on it.)

uv.lock and the requirements/* exports are regenerated — dropping 3.9/3.10
removes most of the version-conditional resolution branches.

tests/test_python_version_policy.py pins the invariant: install.sh's floor,
requires-python, the classifiers and the CI matrix must agree.

* ci: run lint and tests from uv.lock

The lock file and the hash-pinned exports were not enforced anywhere: CI
still built a venv by hand and installed unpinned `PyGObject` plus
`-e ".[dev,vad]"`, so the tool versions it checked against had nothing to do
with the ones `just lint` uses locally.

Both Python jobs now use astral-sh/setup-uv and run through `uv sync
--locked` / `uv run --locked`, which also fails the build when uv.lock drifts
from pyproject.toml.

Partially covers phase 5 of #701 (tests install from the lock, lock freshness
gate); trusted publishing, artifact checksums and action SHA pinning are
still open there.

* ci: lint from a dedicated dependency group

The lint job failed on this branch: `uv run --extra dev` syncs the whole
project, so it tried to build pyaudio from source on a runner that installs
no system dependencies.

    src/pyaudio/device_api.c:9:10: fatal error: portaudio.h: No such file or directory

Move black/isort/flake8 out of the `dev` extra into a `lint` dependency
group. CI runs `uv run --locked --only-group lint`, which installs the three
pure-Python tools and skips the project entirely — no portaudio or
girepository headers needed. `just deps` and DEV_EXTRAS request the group
alongside the dev extras, so the local environment still carries test and
lint tooling at once and `just lint` matches CI version for version.

pre-commit is unaffected: its hooks pin their own tool versions upstream.

* fix(install): never configure an engine the venv cannot import

vosk became an optional extra in #705, but the installer still writes
`"engine": "vosk"` into config.json on the whisper.cpp fallback path even
when installing the extra only produced a warning. config.json also survives
reinstalls — every writer guards on "file does not exist" — so an engine
picked by an earlier attempt outlives the venv that supported it. Either way
Vocalinux dies at startup:

    ModuleNotFoundError: No module named 'vosk'

- the fallback now writes the vosk config only when `import vosk` actually
  succeeds, and otherwise leaves the engine configuration alone
- a new verify_configured_engine() runs near the end of the install: if
  config.json selects vosk and the venv cannot import it, it says so and
  prints both fixes instead of leaving a broken install behind

* feat(models): verify every model download against a pinned digest

Pins a sha256/md5 per downloadable speech model in
src/vocalinux/utils/model_checksums.txt and verifies against it in both
install.sh and the runtime downloaders, before the file is moved into place.
Verification fails closed. Covers phase 2.5 of #701; see the PR for details.

* docs(agents): rebase AGENTS.md onto the #727 rewrite

* fix(install): hash models that are already on disk instead of trusting them

* refactor(models): one download path per engine, and fix Whisper's

* docs(install): drop distros that cannot meet the 3.11 floor

* docs: retire the distros and Python versions the 3.11 floor rules out

* fix(install): abort instead of warning when Python is below the floor

* fix(install): fall back to the system Python, not the one on PATH

* fix(install): match an existing venv by installation, not by X.Y

* fix(install): repair a config naming an engine the venv cannot import

* fix(models): pin VOSK by the sha256 of bytes we fetched, not upstream's md5

* fix(justfile): request the dev set in every recipe that syncs

* test(version): scan the docs and website trees for a stale Python floor

* docs: correct the stale model-checksum and distro claims

* fix(models): stamp the VOSK tree the app extracts, as the installer does

* fix(install): replace an unverified VOSK model only once its successor is ready

* docs(install): align the Fedora floor with the rest of the tree

* test(install): drop an f-string with no placeholders

* test(models): stub the downloader probe the VOSK installer test relies on

* docs: drop the long dashes from two model-checksum comments

* fix(install): gate on the interpreter, not on the distro release label

* fix(justfile): export the lint group into requirements/dev.txt

* fix(models): unpack the VOSK tree into staging and stamp it before the swap

* fix(whisper): adopt a checkpoint an earlier release stored under the catalog name

* docs(settings): state what a stray Whisper copy really means for deletion

* fix(models): hash a whisper.cpp model found on disk before it reaches ctypes

* docs(contributing): stop telling readers to uninstall their linters

* fix(install): stop warning off distros the project documents as supported

* docs(distros): file the supported derivatives under supported
2026-08-24 00:25:06 -07:00
Jatin K MalikandGitHub ba9cca844b chore(release): prepare v0.16.0
Stable minor since v0.15.0. Bumps version to 0.16.0 across app, web, AUR, and Flatpak AppStream, and refreshes docs, changelog, and SECURITY.

Tag v0.16.0 after this lands on main.
2026-08-23 01:33:08 -07:00
Jatin K MalikandGitHub 994dda10a6 fix(install): leftover review notes from #700 and #705 (#706)
* fix(install): leftover review notes from #700 and #705

The scratch dir now holds pip logs and model download temps, so a failed
run has something to inspect. Dep, venv, leftover-process, and pip
failures use exit codes 2/3/4 instead of a blanket 1. --help says the
transcript is written during installation, and the tag hint is
--tag=<release>.

AGENTS.md no longer claims the hash-pinned exports are consumed (they
wait for phases 2/3/5). Docs add a just install line and
vocalinux[vosk] next to the PyPI commands. Settings points at
--engine=whisper. The whisper.cpp fallback installs ".[vosk]".

Fixes leftover review notes from #700 and #705.
Refs #701.

* fix(install): do not let pip build PyGObject from sdist

Fresh --auto --skip-system-deps died on pygobject-3.56.3 even with
distro python3-gi present. Install the other runtime deps from
pyproject.toml, then pip install --no-deps. Extra installs use the
extra's packages only, not pip install ".[extra]".

Fixes leftover review notes from #700 and #705.
Refs #701.

* fix(install): tolerate unset XDG_SESSION_TYPE under set -u

--auto with DISPLAY but no login session died on
XDG_SESSION_TYPE: unbound variable. The session probe now uses
${XDG_SESSION_TYPE:-}, ${WAYLAND_DISPLAY:-}, and ${DISPLAY:-}.
2026-08-18 17:41:37 -07:00
Sergey SilaevandGitHub 95d135ebbf chore(deps): lock dependencies with uv and pin every build input
Phases 0-1 of #701. Drops dead deps, moves vosk to an optional extra, adds uv.lock and hash-pinned requirements exports, pins pywhispercpp to 1.5.0.

Closes #705. Refs #701.
2026-08-18 16:26:13 -07:00
Sergey SilaevandGitHub 71c84fe86b chore(build): harden installer script and replace Makefile with Justfile
Stacked bottom of #701. Replaces Makefile with justfile and hardens install.sh (strict mode, logging, HTTP probes, curl -f on model downloads, no v0.10.1-beta fallback).

Closes #700.
2026-08-18 16:24:11 -07:00
Jatin K MalikandGitHub 279d99fea0 fix(whisper): honor bundled GPU libs and skip software Vulkan devices (#674)
* fix(whisper): use bundled GPU libs and skip software Vulkan devices

AppImage 0.15.0 shipped a CPU-only pywhispercpp wheel, so the Settings
GPU picker and host vulkaninfo never moved inference off the CPU. Rebuild
pywhispercpp with Vulkan in the AppImage, log the libraries that actually
load, skip llvmpipe in auto-select, and document how to verify.

Fixes #604

* fix(appimage): pin gcc/g++ for Vulkan pywhispercpp rebuild

Hosts that default cc/c++ to clang fail CMAKE's C++ test because clang
does not search gcc's libstdc++ path. Default the rebuild to gcc/g++.
2026-08-16 18:29:07 -07:00
Jatin K MalikandGitHub 32a17a9659 chore: point GitHub URLs at VocaHQ (#696)
Repo links, install one-liners, and release checks now use
github.com/VocaHQ/vocalinux. The site mentions VocaHQ without a redesign.

The Codeberg mirror, personal email, and X handle are unchanged.
2026-08-16 15:22:22 -07:00
Jatin K MalikandGitHub ecd5839a13 feat(settings): delete unused downloaded speech models (#671)
* feat(settings): delete unused downloaded speech models

Add a Remove Model dropdown on the Speech Model page so whisper.cpp,
Whisper, and VOSK downloads can be deleted from Settings. The currently
selected model is marked in use and cannot be removed until you switch
away from it.

Fixes #650

* fix(settings): move unused model deletion below the picker

Unused downloads is its own group under the model info card. Only leftover
files appear, each with a Delete button. The group hides when empty.

* fix(settings): collapse unused downloads into an expander

Keep leftover models behind one row so a long list does not stretch the
Speech Model page. Expand to delete files one at a time; the list scrolls
once it grows past a few entries.

* fix(settings): keep leftover model list from clipping in the expander

Wrap the unused-downloads list so the expander can show a few rows and
scroll the rest, instead of painting only the first leftover.
2026-08-12 23:42:36 -07:00
Jatin K MalikandGitHub c337d8f56d feat(shortcuts): default to hold Right Alt (push-to-talk) for new installs (#648)
* feat(shortcuts): default to hold Right Alt push-to-talk on first install

Double-tap Ctrl conflicts with many desktop and app shortcuts, so new
installs now use hold Right Alt (Option) in push-to-talk mode. Existing
config.json values are preserved unchanged.

Updates README, user guide, AGENTS.md, first-run dialog, website copy,
and tests to match the new defaults.

* refactor(shortcuts): trim redundant default-change test and comments

Drop the fresh-install assertion that duplicated DEFAULT_CONFIG coverage,
remove the duplicated Right-Alt rationale from config_manager, and drop a
vacuous source-string check from the settings shortcuts test.

* fix(shortcuts): align install.sh seeds and preserve legacy toggle mode

install.sh still wrote ctrl+ctrl without mode, so merges with the new
push_to_talk default became hold-Ctrl. Seed right_alt + push_to_talk for
new installs, and migrate existing shortcut-only configs to mode=toggle
so prior behavior is unchanged.

* style(config): fix black formatting after shortcuts migration

* fix(shortcuts): preserve legacy defaults when shortcuts section is missing

Existing config files without a shortcuts section previously inherited
ctrl+ctrl toggle from DEFAULT_CONFIG. Pin those historical defaults on
upgrade so the new first-install Right Alt push-to-talk defaults do not
silently change behavior.

* docs(agents): unwrap hard-broken dictation control paragraph

Keep the AGENTS.md paragraph as a single line like the surrounding prose.
2026-08-08 00:07:54 -07:00
06e8809c2a feat(updates): notify when a newer GitHub release is available (#645)
* feat(updates): notify when a newer GitHub release is available

Check the configured release channel shortly after startup and every six
hours. When an update exists, show a tray menu item and a green New badge
on Settings → About, plus one desktop notification per version, so users
can open release notes and update themselves.

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

* refactor(updates): slim UpdateMonitor after ponytail review

Drop unused APIs (tick/check_now/version injector/last_* state), integer-only
GLib scheduling, and the local demo launcher. Keep tray notify-send local to
avoid coupling to recognition_manager.

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

* fix(updates): address Bugbot and harden notify persistence

Persist last_notified_version only after notify-send spawns successfully,
and sync the tray Update Available item from About-page checks without
re-notifying. Also merge latest main and cover the new paths in tests.

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

* test(tray): pin config mock for update-notification assertions

CI on 3.9/3.10 showed empty MagicMock set call history after a successful
notify-send spawn. Pin tray.config_manager to the test mock and record set()
via side_effect so persistence checks are reliable across Python versions.

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

* fix(updates): clear pending About update on failed lookup

A failed About check hid the New badge but left _pending_update set, so
clearing settings search could revive the badge while status still showed
the failed lookup. Clear dialog pending state on failure (tray unchanged)
and restore the badge via _set_about_update_badge.

* fix(updates): ignore stale UpdateMonitor results after channel switch

Bind the channel for each background check and discard the callback when
the configured channel changed mid-flight, then re-check immediately —
matching About-page stale-channel handling so the tray cannot show the
wrong channel's update state.

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
2026-08-08 00:04:39 -07:00
67dde781f5 docs(user-guide): note CUDA device 0 limit for dual NVIDIA (#644)
Document that CUDA-backed pywhispercpp always uses device 0, and
point to CUDA_VISIBLE_DEVICES or a Vulkan build as workarounds.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
2026-08-05 21:56:02 -07:00
9c340029e1 docs(packaging): prefer Ayatana AppIndicator in Fedora/Arch hints (#638)
* docs(packaging): prefer Ayatana AppIndicator in Fedora/Arch hints

Follow-up to #621: align docs, AUR depends, package map, dependency
check messages, and troubleshooting copy with the Ayatana-first install
path so manual installs and AUR users get a working KDE tray icon.

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

* style(main): black-format Ayatana install hint lines

CI black --check failed on the multi-line logger.error calls for the
Fedora/Arch package hints.

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

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
2026-08-04 04:53:34 +05:30
fbb81238f5 chore(release): prepare v0.15.0 (#609)
* chore(release): prepare v0.15.0

Bump version across app, web, AUR, and Flatpak metainfo. Document GitHub
release-notes rules, refresh README/UPDATE/INSTALL/USER_GUIDE for the 0.15
series (sidebar settings, AppImage, auto-pause/keepalive, Vulkan device
selection), and align website product surfaces with shipped features.

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

* docs(release): fold latest main into v0.15.0 notes

Include dictation capitalization/trailing-space, ibus-wayland injection,
IBus teardown, and settings notice polish landed on main after the initial
release prep rebase.

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

* docs(release): include Bluetooth mic crash fix in v0.15.0 notes

Fold #599 (PortAudio Bluetooth SCO probe heap corruption) into README,
UPDATE, changelog, AppStream, and release history after rebasing onto main.

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

* docs(release): include language catalog expansion in v0.15.0 notes

Fold #616 (Hungarian and expanded speech languages, Fixes #565) into
README, UPDATE, changelog, AppStream, PRODUCT.md, and release history
after rebasing onto latest main.

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

* docs(release): fold settings footer and language follow-ups into v0.15.0

Include #618 sidebar dictation footer, #619 Custom Shortcut Record/Set
restore, and #617 English (India) Whisper language mapping after rebasing
onto latest main.

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

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
2026-08-02 15:26:03 -07:00
40546e7f07 fix(uninstall): remove IBus data dir & stop app by PID file not pgrep -f (#597)
* fix(uninstall): remove IBus runtime data directory on uninstall

Discussion #559 reported ~/.local/share/vocalinux-ibus surviving uninstall.
PR #569 fixed launcher wrappers but not this IBus IPC directory (socket,
PID file, stderr log). Stop the engine via engine.pid when present, remove
the directory regardless of --keep-data, and verify it in post-checks.

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

* fix(scripts): stop Vocalinux by PID file, not pgrep -f

pgrep -f vocalinux matches any process whose argv mentions the repo
path (editors, shells, test runners) and can kill unrelated local work.
AGENTS.md already warns against this.

- Prefer PIDs from instance.lock and vocalinux-ibus/engine.pid
- Verify each candidate via is_vocalinux_process (python -m vocalinux.main,
  ibus_engine.py under vocalinux, or argv0 basename vocalinux/vocalinux-gui)
- Narrow pgrep fallback only for those entrypoint patterns
- Drop the redundant stop_ibus_engine helper (engine covered via PID file)
- Still remove ~/.local/share/vocalinux-ibus on uninstall (#559)
- Align install.sh and correct the pkill tip in docs/UPDATE.md

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

* fix(scripts): detect setuptools console-script Vocalinux processes

Real installs run as `python …/bin/vocalinux`, so argv0 is python and the
previous argv0-basename check missed the live app (instance.lock PID was
skipped). Scan all cmdline args for …/bin/vocalinux or …/bin/vocalinux-gui
instead, which still avoids matching a bare repo directory path.

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

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
2026-07-26 21:14:19 -07:00
Anay GarodiaandGitHub 2a0e11e077 fix: correct ydotool service setup guidance (#560) 2026-07-25 13:12:32 -07:00
Jatin K MalikandGitHub 8e0439e359 docs: multi-distro tray icon FAQ (apt, dnf, pacman) (#584)
Tray troubleshooting previously only showed apt, which is wrong on Fedora
and Arch. Document AppIndicator install for Debian/Ubuntu, Fedora, and
Arch in INSTALL.md and the website troubleshooting page.

Fixes #570
2026-07-23 23:23:29 -07:00
Jatin K MalikandGitHub 3d65e5cea9 chore(release): prepare v0.14.2 (#537)
* chore(release): prepare v0.14.2

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

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

* fix(release): address 0.14.2 review findings

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

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

Cover #538 and #541 (scrollable settings tabs / wheel polish) in README,
UPDATE, website changelog, AppStream, and version history after rebasing
onto main.
2026-07-17 18:42:53 -07:00
Jatin K MalikandGitHub 1783331c0d chore(release): prepare v0.14.1 (stable for Flathub) (#532)
Drop beta suffix so Flathub stable AppStream lint accepts the release.
2026-07-16 19:00:48 -07:00
Jatin K MalikandGitHub 853c1504b2 chore(release): prepare v0.14.1-beta (#525)
Patch release for Flatpak/AUR packaging and post-0.14.0 fixes:
installer wrappers, ydotool dual API and Flatpak socket probe, model
download timeouts, layout-aware hotkeys, and related docs/web bumps.
2026-07-16 17:51:45 -07:00
Jatin K MalikandGitHub 3deb523efd feat(flatpak): add Flatpak packaging for universal distribution (#484)
* feat(flatpak): add Flatpak packaging for universal distribution

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

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

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

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

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

Closes #167

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

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

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

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

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

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

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

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

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

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

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

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

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

Matches the shipped VOCALINUX_YDOTOOL_KEY_DELAY default used when
clipboard paste falls back to character typing.
2026-07-16 00:03:30 -07:00
Jatin K MalikandGitHub 6545e4ea66 feat(packaging): add AUR release package and CI publish (#518)
* feat(packaging): add AUR release package and CI publish

Ship packaging/aur/vocalinux for yay installs of tagged releases, wire
AUR publish into the release workflow (skipped until secrets are set),
and document one-time maintainer setup.

Fixes #116

* refactor(packaging): slim AUR PR to PKGBUILD + CI

Drop systemd unit, install script, website edits, and long docs.
Keep release PKGBUILD, minimal publish job, and short AUR setup notes.
2026-07-14 23:41:31 -07:00
Jatin K MalikandGitHub 8a085b5395 chore(release): prepare v0.14.0-beta
* chore(release): prepare v0.14.0-beta

- Bump version 0.13.0-beta -> 0.14.0-beta (src/vocalinux/version.py)
- README: refresh What's New for v0.14.0-beta and bump comparison table
- docs/UPDATE.md: add v0.14.0-beta section and bump checkout example
- SECURITY.md: mark 0.14.x supported, 0.13.x unsupported
- docs/RELEASE_PROCESS.md: add v0.14.0-beta to version history
- web: bump package.json / package-lock.json / page.tsx schema + badge
- web changelog: add v0.14.0-beta entry

* ci(codecov): allow 0.5% project coverage threshold

Prevents version-bump/docs-only PRs from being blocked by minor
coverage fluctuations (e.g. -0.04% on a release-prep branch).

* docs(release): correct chronological order in version history table

* docs(release): add #509 shortcut UI exclusivity fix to v0.14.0-beta notes
2026-07-13 23:44:07 -07:00
Jatin K MalikandGitHub 85927be4b4 feat(asr): FunASR/SenseVoice support for the Remote API engine (#468)
* feat(asr): add chat audio remote benchmarking

* style(asr): apply black formatting

* test(main): include remote API model kwarg

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

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

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

* feat(asr): support FunASR remote responses

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

Drop chat-completions/Qwen client, evaluation notes, and the remote ASR
benchmark harness. Keep model field, tolerant OpenAI response parsing,
SenseVoice label stripping, FunASR docs, and tests.
2026-07-13 15:03:19 -07:00
Jatin K MalikandGitHub 8f32f01f0e fix(installer): include xsel for Wayland clipboard fallback (#496) 2026-07-02 10:33:28 -07:00
Jatin K MalikandGitHub 63c1053d8b chore(release): prepare v0.13.0-beta (#489)
- Bump version 0.12.0-beta -> 0.13.0-beta (src/vocalinux/version.py)
- README: refresh "What's New" for v0.13.0-beta and bump comparison table
- docs/UPDATE.md: add v0.13.0-beta section and bump checkout example
- SECURITY.md: mark 0.13.x supported, 0.12.x unsupported
- docs/RELEASE_PROCESS.md: add v0.13.0-beta to version history
- web: bump package.json / package-lock.json / page.tsx schema + badge
- web changelog: add v0.13.0-beta entry and refresh dateModified
2026-07-01 17:32:36 -07:00