niri accepts lowercase and mixed-case key names such as mod+shift+m, so
the grep in the conflict check must ignore case too. strings comes from
binutils, which is now said in the docs.
Refs #45
Mod+Shift+D is Move Column Right in Dank Material Shell's niri binds
(dms/binds.kdl), so the shipped example competed with it silently. The
toggle moves to Mod+Shift+M, which is free in niri's stock binds and in
DMS's binds. The start and stop examples move off Mod+Shift+E, which is
stock niri's quit, to Mod+Shift+S and Mod+Shift+X.
niri validate rejects a key bound twice in one binds block but not across
blocks, so validation cannot show a conflict. The docs now give a grep
over the config tree and niri's embedded stock binds for checking a key
before binding it. The overlay pointer is corrected to Mod+Shift+Slash.
A test pins the doc and the include to one toggle key that is not
Mod+Shift+D.
Fixes#45
The injector never offers the RemoteDesktop portal on niri, but inject-probe
ran the portal probe unguarded and reported portal_available: true there.
The probe now reuses the injector's _is_niri_session guard: on niri it skips
the portal and reports portal_skip_reason "niri" with portal_available false.
Off niri the probe runs and reports its answer as before.
Fixes#46
The missing-dependency error gives the Void xbps instruction instead of piping upstream's installer, the IBus hint points at the fork's docs (niri names its own notes), and the IBus engine homepage is the fork. VocaGateway stays upstream, with the credit stated in code and docs.
voculinux now depends on python3-PyAudio and python3-pysocks at the carried versions, so a stock Void package below the project floors is upgraded rather than silently kept. The container smoke installs the stock packages first and asserts the upgrade.
User-facing docs, About page and website now point at the fork and the Void xbps repository, with one credit line to upstream Vocalinux. Channels this fork does not publish are removed from navigation.
Every release is built from its tag alone by packaging/void/build-release.sh, shared by the tag workflow and local dry runs. Signs in a network-less container with the key mounted read-only and publishes to the tag release and the rolling latest release.
CUDA toolkit detection also checks CUDA_HOME, CUDA_PATH and /usr/local/cuda*, and the backend order names only backends the installed build compiled in. An explicit IBus pin on niri is honoured with one warning. The update checker queries this fork's Gitea releases and packaged installs make no network request. The one-time legacy import repoints a migrated dictionary path at the copied file.
Model recommendation keyed to the installed build and free memory, fork-sourced Void templates, signed xbps release workflow on Gitea with the pinned repository key, niri injection guard and wtype-first order, tightened naming sweep, web copy fixes, and CI security and duplication fixes.
Ticket #31 review found startup and demote paths preferring ydotool
over wtype, with a KDE ydotool-first exception, contradicting the
decided order in spec #16 (portal first where a backend implements
it, then direct keysym typing, then the virtual-input tool, then
XWayland-only injection, then manual paste).
- Auto-selection is now portal, wtype, ready ydotool, daemonless
ydotool, xdotool on every desktop including KDE; the __init__ wtype
probe validates wtype and demotes on compositor rejection.
- Portal-deny demotion, IBus-fallback switch, XWayland recovery, and
a new wtype runtime-failure retry all follow the same rung order
with partial-type accounting preserved.
- Ladder suite grows to 14 tests pinning KDE portal selection,
wtype-before-ydotool selection, and each demotion trigger.
- USER_GUIDE states the full rung order and portal-first on KDE.
Add build-xbps tag job to release.yml: verifies the tag against
src/vocalinux/version.py, fails closed when XBPS_SIGNING_KEY is unset,
builds and signs via podman-only repo.sh, and attaches packages,
signatures, repodata, and client key files to the release. Fold the
xbps-repo artifact into publish-checksums (manifest + SHA256SUMS).
Document the Void install in the root README (glibc x86_64 scope,
fingerprint check, third-party repo caveat) and the post-tag template
version/checksum bump as a pull request in docs/RELEASE_PROCESS.md.
Proof: local podman run with a throwaway RSA4096 key built all four
templates, signed packages + repodata, and verified voculinux installs
from the signed repo in a clean rootdir (fingerprint
eb:29:5d:9e:5c:94:c1:63:ae:35:cd:35:db:7a:28:da).
Ticket xavierk/voculinux#27, built on #18 (rename) plus the #25/#26
dependency templates. The template builds the app from the canonical
checksum-verified release sdist, depends on Void's interpreter, GTK,
desktop bindings and packaged Python libraries, and installs the
renamed desktop entry, icons and a packaged-install marker. No speech
models, no vendored wheels, no pip at build or install time.
Packaged installs keep background update checks as a signal, but every
update affordance presents the package-manager path
(src/vocalinux/utils/packaging.py). Also corrects the Void distro
inventory (Ayatana provider name; ydotool marked absent) and regens
the installer map.
Locks the ordered rungs without real compositors: portal first where a
backend implements it (GNOME), direct typing first on niri with IBus
excluded and no portal selected, deny demoting to direct typing with the
full dictation kept, terminal probe choosing Ctrl+Shift+V, held modifiers
delaying the XWayland paste, and clipboard manual paste as last resort.
USER_GUIDE states the niri first run (no prompt, typing goes direct,
never pin portal there) and the GNOME/KDE grant, persistence and deny
fallback under the renamed backend setting and override names.
Refs #31
Docs-only slice of ticket #22: include file, integration page, index
links. Tray-watcher observation stays open for hardware run (#32); no
probe code changed.
Refs #22
The fork takes its own name everywhere a user can see it: binaries
(voculinux, voculinux-gui), desktop entry, autostart, tray, D-Bus
namespace (com.voculinux.Voculinux, no alias for the old name),
config/data/cache dirs, single-instance lock, IBus engine identity,
tray icons, resource prefixes, VOCULINUX_* environment names, and the
update-checker user agent. The internal Python module stays vocalinux
and the distribution metadata keeps the upstream URLs as provenance.
On first run a previous Vocalinux install's config.json, custom
dictionary, and portal restore token are copied once into the new
locations (new config always wins, no merge, no write-back); models
re-resolve through the pinned-checksum download path. An offline
naming-sweep test proves no old-name strings remain on user-visible
surfaces outside the allowlist.
Refs #18
Ticket #19 (spec #16): fixed three-metric protocol as a runnable script
(idle memory both keep-alive states, wake latency from debug logs,
real-time factor on the fixed 30 s sample per backend) plus per-rung
load footprints, portal/direct injection probe, and threshold
derivation for the model-ladder ticket (#20). Offline unit tests for
the parsing/statistics/report helpers. Evidence recorded on the ticket.
One debug-only marker (Toggle requested in state ...) makes wake
latency derivable from logs alone.
Intake at release-preparation sync points plus on-demand critical
fixes, freeze once the release branch opens to release-blockers only,
one upstream fix per fork pull request through the standard gates,
conflicts resolved by keeping upstream logic and re-applying fork
renames, and the qualification rule (fixes and user-instruction docs
port; features re-decided; dependency/CI churn only on a security
advisory). Names the retained internal vocalinux module as the reason
conflicts concentrate at renamed user-visible surfaces.
Tests: tests/test_upstream_intake_policy.py (7 tests)
The Settings audio status label is set with Pango markup. A microphone
named with '&' or '<' (or an audio test error carrying them) made the
markup invalid, so GTK dropped the text. Escape both with
GLib.markup_escape_text.
Also correct the v0.17.0 notes in docs/UPDATE.md: the Snap never
bundled VOSK; it installs the project without extras.
* fix(snap): build and verify payload from locked python inputs
* docs(snap): document locked inputs and artifact verification
* test(clipboard): isolate restore workers between tests
* test(packaging): tighten isolation and clarify snap audit guarantees
* fix(ci): run snap recipe tests on snap-only changes
A change to snap/snapcraft.yaml or snap/local/smoke-test.sh only matched
the snap filter, so the Python tests that guard the recipe (including the
portable whisper.cpp flags from #957) and the smoke script were skipped.
Match all of snap/ in the python filter, as scripts/ already is.
* docs(snap): use a version placeholder in the smoke-test example
---------
Co-authored-by: Jatin <jatinkrmalik@gmail.com>
* chore(release): v0.18.5
- Update version from 0.18.4 to 0.18.5
- Add v0.18.5 sections to CHANGELOG, README, docs/UPDATE.md, metainfo.xml and the website changelog
- Bump website package metadata and softwareVersion schema
- Add the 0.18.5 row to the RELEASE_PROCESS version history
* docs(release): qualify the clipboard-history claim for wl-copy 2.2+
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* fix(audio): bound capture reads and support cancellation
* fix(recognition): bound and serialize capture shutdown
* fix(recognition): cancel retries and report stalled microphones
* fix(audio): prefer pulse when the ALSA default is unavailable
* fix(recognition): guard late callbacks and finalize capture tests
* docs(audio): explain input selection and stall recovery
* fix(speech): enforce auto-pause across buffered reload timeouts
* fix(speech): recover auto-resume after reload timeout
* test(speech): fix auto-pause follow-up test regressions
* fix(speech): retain auto-resume after reconfigure timeout
* fix(tray): preserve auto-pause recovery across suspend
* test(capture-shutdown): give the bare tray the suspend path's shortcut managers
_on_system_suspend now releases the main and per-language hotkey
listeners before sleep (#1025); the fixture builds TrayIndicator without
__init__, so it needs the two attributes present.
* fix(tray): release hotkey listeners on suspend even while auto-paused
Merging #1025's suspend cleanup with this branch's auto-pause handoff
left an early return that skipped shortcut_manager.stop() whenever a
configured game was active, keeping EVIOCGRABs held through sleep. The
paused branch now only owns the model-lifecycle work; listener release
and stopping active recognition run for every suspend.
* fix(tray): hold the apply lock only around the download's verify and save
Wrapping the whole recommended-model download in _apply_settings_lock
blocked every GTK-thread settings apply (e.g. Test Dictation) for the
length of the transfer. The engine already serializes reconfigures, so
the lock only needs to cover verifying the download still owns the
receipt and saving it; an apply that lands mid-download now proceeds
instead of freezing the app.
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* fix(installer): add upgrade mode that keeps the existing configuration
Re-running install.sh on an existing install replayed first-time setup:
the mode menu offered only interactive or automatic, automatic mode
forced whisper.cpp regardless of the configured engine, and nothing said
config.json would be kept, even though docs/UPDATE.md promised the
installer updates in place and preserves configuration.
Detect an existing install early (venv activate + config.json) and offer
an upgrade path:
- The curl|bash mode menu gains 'Upgrade (keep current settings)' as the
default when an install is detected; the guided setup offers it first
too.
- A --upgrade flag runs the same path non-interactively for scripts, and
falls back to a normal automatic install when nothing is installed.
- Upgrade mode reads speech_recognition.engine from the existing
config.json instead of forcing whisper.cpp, reuses the venv, updates
that engine's dependencies, and rebuilds pywhispercpp only when the
installed version drifts from the pinned release or GPU support is
missing.
- config.json is never modified: every engine path that may rewrite it
(parakeet, faster_whisper, the whisper.cpp -> vosk fallback) goes
through a wrapper that is a logged no-op in upgrade mode, and
verify_configured_engine reports an unimportable configured engine
instead of repairing the file.
- The installer prints 'Existing configuration found at
~/.config/vocalinux/config.json - it will be kept.' when it detects
one.
Update docs/UPDATE.md with the upgrade behavior and add
tests/test_installer_upgrade.py covering detection, the mode menu
default, engine resolution from config, config immutability, the guided
offer, --upgrade parsing, and an end-to-end pty run of the piped
installer.
Fixes#1009
* test(installer): isolate the upgrade pty test from host changes
Skip system-dependency and model steps in the end-to-end installer run
so the only host-mutating calls are gated out entirely; the timed
interrupt is no longer the only thing between the marker and sudo.
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* fix(aur): gate source publication on attested sdists
Prepare and verify the canonical release source, build the exact PKGBUILD on Arch, and publish the gated artifact without further rewrites. Part of #701.
* fix(aur): isolate local gate and binary publication
Keep local gate distributions and diagnostics in a private workspace, preserving other build outputs and failed diagnostics. Publish vocalinux-bin after its own AppImage inputs without depending on the source gate or unrelated release jobs. Add regression coverage for both review findings.
* fix(clipboard): keep paste-injection copies out of clipboard history
The clipboard-paste injection path writes the dictated text to the
clipboard only to feed the paste chord, then restores the previous
content (#588, #646). Clipboard managers such as Klipper and cliphist
still record that transient copy, so every dictation lands in history.
Mark the injection copy with `wl-copy --sensitive`, which clipboard
managers honour by skipping the entry. The flag is probed once via
`wl-copy --help` (added in wl-clipboard 2.2) so older builds keep
working. It is only set when Copy to Clipboard is off: with that
setting on, the dictated text is meant to stay, so it remains an
ordinary copy. Restoring the user's previous content is never marked
sensitive. xclip/xsel have no equivalent and are unchanged.
* test(clipboard): address review on sensitive paste copies
- Move the unittest main guard below TestSensitivePasteCopy so running
the file directly also runs the new tests.
- Add -> None return hints to the new test methods.
- Cover _copy_to_clipboard forwarding the sensitive hint when a tool
declines and the next one is tried.
- Docs: history suppression needs wl-copy 2.2+ and a clipboard manager
that honours the hint; older wl-copy and the XWayland xclip/xsel path
still produce ordinary copies.
* fix(clipboard): type the wl-copy probe cache and adapt the X11 paste test
- Declare _wl_copy_sensitive_supported as a typed class default instead
of reading it through getattr(), which mypy (now in CI since #968)
flags as returning Any.
- test_plain_x11_clipboard_paste_uses_xdotool_and_x11_clipboard
asserted the exact _copy_to_clipboard kwargs. Check the text and the
X11 tools instead, so it does not depend on the sensitive hint (which
xclip ignores) or on the copy_to_clipboard setting.
* test(clipboard): wait for the restore thread instead of a fixed sleep
TestSensitivePasteCopy slept 0.5 s and hoped the background restore had
run. On a slow runner the patches could end first. Wait on an event set
by the restore copy, inside the patches, with a generous timeout.
Inside the snap, list the sound server's capture sources with pactl and record from the chosen one by scoping PULSE_SOURCE to each PortAudio open (serialized behind one lock). Settings saves the stable source name, keeps each dropdown entry's server name so selection never queries the server from the GTK thread, and the snap reports whisper.cpp as CPU-only instead of probing host GPU tools.
Fixes#1011
* feat(build): add locked reproducible wheel gate
Build independently in clean source trees and virtualenvs, compare wheel bytes, and stage only verified canonical distributions. Safely clean recognized prior outputs on repeat runs.
Part of #701
* ci(release): verify wheel reproducibility before publishing
Run the shared gate in PR and tag workflows, preserve failure diagnostics, and keep verification artifacts out of release checksums. Make Snap Store publication wait for the gate and reuse the canonical distributions for PyPI.
Part of #701
* docs(release): document reproducible wheel validation
Describe the locked toolchain, canonical publication boundary, same-platform scope, diagnostic outputs, and safe automatic cleanup.
* chore(scripts): update executable permissions
* fix(build): isolate release tooling and preserve cleanup retries
Exclude host site-packages from build-only virtualenvs so unlocked setuptools plugins cannot affect verified distributions. Delete staged artifacts before canonical ownership evidence to recover safely from interrupted cleanup.
Add regression tests and document the build-only environment exception.
* fix(release): make reproducible builds retryable and refresh tools
Regenerate release build exports from uv.lock. Hold a checkout lock through cleanup and staging, and atomically rename complete archives into dist. Cover interrupted staging, competing processes, and lock release with regression tests.
---------
Co-authored-by: Kanishk Pachauri <itskanishkp.py@gmail.com>
Co-authored-by: sesav <1636488+sesav@users.noreply.github.com>
- Update version from 0.18.3 to 0.18.4
- Add v0.18.4 sections to CHANGELOG, README, docs/UPDATE.md, metainfo.xml and the website changelog
- Bump website package metadata and softwareVersion schema
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* fix(settings): make model downloads explicit and cancel the whole intent
"Use it" on the recommendation card applied a different model than the one
shown, a cancelled or failed apply left the staged model behind so the next
Test Dictation downloaded it, and cancelling a download tore down and then
reloaded the previous model while the dialog waited.
- Recommendation is now passive text, compared by size tier, with a link to
docs/MODEL_SELECTION.md. The "Use it" button is gone.
- Every Settings download asks first and shows size and source host.
Declining, cancelling or failing drops the staged model spec and puts the
pickers back on the saved config.
- Test Dictation refuses a model that is not on disk instead of applying it.
- reconfigure() fetches a missing model before releasing the loaded one, so
a cancel restores state without reloading or rehashing the old model.
Cancellation state is reset only when a download claim is taken or ended.
- Follow-layout mode shows the resolved language in the simple Main language
control, read-only, and simple edits keep the "layout" setting.
Fixes#979Fixes#980Fixes#981Fixes#982Fixes#985Fixes#988
* fix(settings): restore follow-layout mode when a model change is declined
* fix(settings): keep follow-layout and dictation consistent during model changes
* style(tests): format follow-layout regression tests
* fix(recognition): skip resume reinit while a model download is running
* fix(recognition): keep a lazy reload pending when resume is skipped during a download
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* fix(hotkeys): use the X11 listener for Flatpak on native X11 sessions
Use evdev for Flatpak Wayland/XWayland while preserving the GNOME X11 fallback.
Fixes#971
* docs(flatpak): note that GNOME X11 still needs /dev/input for hotkeys
* fix(perf): reduce startup and inference overhead
* fix(perf): report effective whisper.cpp fallback settings
* fix(injection): add opt-in X11 paste for long dictations
* perf(vad): lower default silence timeout to 1.2s
* fix(types): resolve mypy errors across application source
* ci: run mypy in the lint job
* ci: include mypy in lint dependency group
* fix(types): narrow evdev timestamp comparison
* refactor(recognition): add SpeechEngine protocol and engine dispatch table
* refactor(settings): move About section and engine tables out of settings_dialog
* docs: add ARCHITECTURE.md and refresh docs index and AGENTS layout
* fix(settings): preserve GTK dialog compatibility after extraction
* style: format touched Python modules
* fix(types): keep local typecheck from syncing dependencies
* test(vad): align main startup expectation with new default
* style: sort engine registry and protocol test imports
* test(settings): follow extracted About builder attribute alias
* test(settings): isolate GTK dialog base in About test
* test(settings): isolate About color scheme build test
* test(settings): restore GTK base after About scheme test
* test(settings): isolate About scheme check from GTK mocks
* test(settings): support extracted dialog mixin methods
* docs: add before/after performance benchmark charts
* fix(injection): type instead of pasting when the clipboard holds non-text data
* chore(release): v0.18.3
- Update version from 0.18.2 to 0.18.3
- Add v0.18.3 sections to CHANGELOG, docs/UPDATE.md, metainfo.xml and website changelog
- Bump website package metadata and softwareVersion schema
- Clarify that the uinput-carrying sideload snap is the v0.18.2 asset
* docs(release): include the transcribe modal-grab fix (#965) in v0.18.3 notes
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
The credential gate silently skipped the remote publish when
FLATPAK_GPG_PRIVATE_KEY / FLATPAK_REPO_TOKEN were unset, so the
documented auto-update remote could stay unpublished for releases at a
time (#951). Emit an Actions warning annotation and a run-summary entry
instead of a plain log line, and note in docs/FLATPAK_REMOTE.md that any
token with Contents write on the remote repo works.
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-10-16 09:20:44 -07:00
devin-ai-integration[bot]GitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Jatin K MalikDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(snap): drop uinput plug denied by the Snap Store
The Snap Store rejected revisions 8-10 over the super-privileged uinput
plug (allow-installation), and reviewers denied the grant request on
forum.snapcraft.io/t/53528, pointing at the RemoteDesktop portal path
the app already prefers. Remove the plug and the uinput-only ydotool so
revisions pass automatic review again; Wayland typing continues through
the RemoteDesktop portal (already the default backend ahead of ydotool),
wtype, IBus Wayland on KDE, and clipboard paste.
KDE Plasma loses its correct-case ydotool path on the store snap (KWin's
portal mangles keysyms on some layouts, KDE bug 489021); the v0.18.x
GitHub .snap still declares uinput for KDE users who sideload it.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix(text-injection): stop telling snap users to connect uinput
The Store snap no longer declares the plug, so the warning pointed at a
command that cannot work. The Snap hints now explain that only the
v0.18.x GitHub sideload declares uinput, and that native Wayland typing
uses the RemoteDesktop portal or IBus Wayland on KDE.
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>
* chore(release): v0.18.2
* chore(release): add Flatpak release notes and refresh changelog date
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* feat(nix): add flake package, NixOS and Home Manager modules
Add a flake.nix plus packaging/nix derivations so Nix and NixOS users can
run and install Vocalinux declaratively, including via Home Manager.
- package.nix: buildPythonApplication with GTK3, libayatana-appindicator,
IBus and libnotify typelibs; wraps the entry point with every host tool
the injectors probe for (xdotool, wtype, ydotool, wl-clipboard, xclip,
xsel, xprop, setxkbmap, ibus, gdbus, notify-send, paplay, aplay)
- pywhispercpp.nix: builds pywhispercpp from its vendored PyPI sdist
(absent from nixpkgs, same gap as Debian/Fedora), static whisper.cpp,
GGML_NATIVE=OFF for a portable baseline
- module.nix: NixOS module enabling hardware.uinput and installing the
package; input/uinput group membership stays a per-user choice
- home-manager.nix: programs.vocalinux.enable installs into the profile
- docs/INSTALL.md gains a Nix / NixOS section; packaging/nix/README.md
carries the usage snippets
Refs #931
* fix(nix): address review findings on imports check and docs
- pywhispercpp: check pywhispercpp.model and _pywhispercpp too, so the
compiled binding is proven loadable, not just the package
- README Home Manager example: declare the nixpkgs/home-manager inputs
and pass pkgs, which homeManagerConfiguration requires
- package.nix: use top-level xprop/setxkbmap attrs (xorg.* is deprecated)
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
Prepares the v0.18.1 patch release: version bump, changelog and docs sync, native-package and web metadata, and release notes crediting every reporter and PR author.
* 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>
* 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.
* chore(release): prepare v0.18.0
Bump version surfaces for the v0.18.0 minor release: version.py,
web/package.json + lockfile, softwareVersion schema, SECURITY.md
supported-versions table, AUR pkgver/_tag (source and -bin),
AppStream metainfo release entry, and the release-history table in
RELEASE_PROCESS.md.
Docs and site copy for the delta since v0.17.0: README blurb, snap
sideload versions in README/INSTALL/UPDATE, CHANGELOG current-stable
entry, docs/UPDATE.md What's New with the 0.18 highlights table and
per-PR credits, website changelog + desktop-reliability entries, and
PRODUCT.md capability lines for Dictation Pad, per-language shortcuts,
tray history, overlay, ducking, PipeWire, portal injection, D-Bus
activation, custom dictionary, and file transcription.
* fix(release): address review findings on the v0.18.0 prep
- Keep the checked-in vocalinux-bin PKGBUILD on v0.17.0 so its real
AppImage digests stay consistent with its sources; release.yml bumps
pkgver/_tag and pins both-arch digests atomically at publish
- Qualify the Flatpak release note: PipeWire system-audio capture and
RemoteDesktop portal injection are native-install paths
- Refresh dateModified on the changelog and desktop-reliability pages
- Restore the release-history table order in RELEASE_PROCESS.md
- Revert the build-regenerated web/next-env.d.ts route-types reference
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* 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>
* 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>
* 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>
* 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>
* 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>