Commit Graph
524 Commits
Author SHA1 Message Date
c59fe8b577 fix: skip unbridged IBus on KDE Plasma Wayland (#577)
* fix(injection): skip unbridged IBus on KDE Plasma Wayland

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

Fixes #574

* refactor(injection): tighten KDE VirtualKeyboard bridge check

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

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

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

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

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

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
2026-07-26 23:06:26 -07:00
4c070ded46 feat(offload): auto-pause apps + idle keep-alive model unload (#592)
* feat(auto-pause): unload speech model while selected apps run

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
2026-07-26 22:29:00 -07:00
Jatin K MalikandGitHub b945137d60 fix(vulkan): auto-select discrete GPU and add manual device selection (#590)
* fix(vulkan): auto-select discrete GPU and add manual device selection

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

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

Fixes #589

* test: add coverage for Vulkan GPU device selection

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

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

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

Fixes #589

* fix(speech_recognition): fallback when pywhispercpp lacks context_params

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

Fixes #589

* refactor(speech_recognition): inline context_params assignment

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

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

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

Fixes #589

* refactor(whispercpp): drop ineffective GGML_VULKAN_DEVICE env var

pywhispercpp ignores GGML_VULKAN_DEVICE; device selection already goes
through context_params={"gpu_device": N}. Remove the misleading env write
so maintainers do not think the env var is the selection path.
2026-07-26 21:25:20 -07:00
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
44dac3b4ab docs(mirror): add Codeberg mirror setup and optional sync workflow (#595)
* docs(mirror): add Codeberg mirror setup and optional sync workflow

Document one-time Codeberg migration (pull mirrors are disabled on
Codeberg.org) plus an optional GitHub Actions push for ongoing sync.

Links the expected mirror URL from README and CONTRIBUTING.

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

* docs(mirror): drop maintainer setup doc from PR

Keep only the README mirror table and optional sync workflow.
Setup notes are one-off and do not belong in the repo.

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

* chore(ci): remove obsolete mirror setup comment

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 18:14:09 -07:00
e097a6977f docs(agents): document Cursor Cloud dev environment setup (#594)
* docs(agents): add Cursor Cloud dev environment setup notes

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

* docs(agents): note install.sh --dev --auto usage in cloud env

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

* docs(agents): document running the desktop GUI app (tray + virtual mic) in cloud VM

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

* chore(skills): add humanizer and ponytail agent skills for all repo sessions

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

* docs(agents): note AppIndicator + librsvg2-common (SVG loader) needed for tray icon rendering

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

* chore(skills): remove committed skills; install at user level (~/.cursor/skills) instead

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

* docs(agents): reflow Cursor Cloud section to free-flowing lines; note pre-installed skills

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

* docs(agents): reference ~/.local/bin/vocalinux-gui-env.sh GUI bring-up helper

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 17:38:58 -07:00
Anay GarodiaandGitHub 2a0e11e077 fix: correct ydotool service setup guidance (#560) 2026-07-25 13:12:32 -07:00
Anay GarodiaGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
eb16a34328 feat(cli): add --version option 🤖🤖🤖 (#563)
* feat(cli): add version flag

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

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-25 13:04:52 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
81bfe9345b chore(deps): bump next (#587)
Bumps the npm_and_yarn group with 1 update in the /web directory: [next](https://github.com/vercel/next.js).


Updates `next` from 15.5.20 to 15.5.21
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v15.5.20...v15.5.21)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 15.5.21
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 12:30:48 -07:00
Jatin K MalikandGitHub b501235f1e feat(web): redesign marketing site with workstation craft (#582)
* feat(web): deslop marketing site with workstation craft identity

Replace generic AI SaaS landing patterns with a deliberate Linux desk
utility look: cool paper palette, Bricolage + Source Sans, solid type,
editorial feature rows, and terminal install as first-class proof.

Adds PRODUCT.md, DESIGN.md, and WEB-DESLOP-NOTES.md. Drops unused geist
and react-intersection-observer. No product claim changes.

* chore(web): drop local deslop working notes from PR

WEB-DESLOP-NOTES.md was a firstmate/session artifact, not product docs.

* fix(web): stop mobile horizontal overflow and harden viewport layout

Long install URLs expanded the page past the phone width. Constrain
terminal pre with break-all, min-w-0 columns, and overflow-x clip.
Tighten mobile hero type and full-width CTAs. Add Playwright viewport
QA script and jest checks that assert no overflow at desktop/tablet/mobile.

* refactor(web): drop browser speech preview from marketing home

Remove the cloud SpeechRecognition demo section and nav Demo link.
Keep product truth: hero install, features, screenshots, engines.
Fold shortcut/engine/audio links into the features intro.
Delete unused LiveDemo component and its unit test.

* chore(web): remove orphaned demo overlay and double-press hook

Only LiveDemo used these; gone with the browser preview section.

* feat(web): borrow PR #546 craft language into deslop home

Merge closed redesign language onto feat/web-deslop-craft: pure white
and near-black iron surfaces, emerald-committed palette, asymmetric
hero with real app screenshots, full-bleed install strip, feature bento
with tray/settings shots, and install-to-dictation steps.

Keep Bricolage + Source Sans (not Geist), no browser speech demo, and
mobile overflow guards. Update screenshots homepage expectations.

* docs(design): align DESIGN.md tokens with PR #546 iron/emerald palette

* fix(web): drop settings screenshot from installer details

Uninstall sat too far below the list once the general-settings shot
was in that column. Keep checklist, requirements, and uninstall only.

* fix(web): raise light-mode primary and footer contrast

Darken light emerald so white button text hits ~5:1 (was 3.4).
Brighten install-strip and footer secondary text for legibility.
Verified light/dark × desktop/tablet/mobile with no overflow.

* fix(web): compact feature cards to kill empty vertical space

Right-column tiles were h-full + justify-between next to a tall
screenshot, so short copy left large voids. Use content-height cards
with denser padding and inline link rows instead.

* fix(web): pair settings shot with full feature rail

Replace three orphan cards beside a tall dialog with a six-row
feature panel that stretches with the screenshot frame. Crop the
settings image with object-cover/object-top so heights match and
no void sits next to the UI shot.

* feat(web): apply homepage craft language to SEO child pages

Rewrite SeoSubpageShell chrome (fixed nav, install pill, iron footer)
to match the deslop home. Add subpage utilities and retoken all
templated child routes: borders, radii, mono kickers, single emerald
accent, terminal panels, no rainbow icon tiles or gradient cards.

* fix(web): avoid invalid @apply hover:border-primary/40 in subpage-card

* fix(web): rewrite subpage CSS without invalid Tailwind @apply

* fix(web): color comparison X icons rose-red on alternatives

Use soft rose (#c45c5c / #e08b8b) for no/cons marks so they read as
distinct from emerald yes/checks without looking like form errors.

* docs(web): move product/design docs under web/ and drop impeccable markers

Keep PRODUCT.md and DESIGN.md next to the Next app (not repo root), add
web/AGENTS.md as the entry point, and point the root AGENTS website
section at those files. Strip tool-specific schema comments.

* fix(web): use icon-only copy control on install terminal

Replace the chunky Copy/Copied chip with a quiet clipboard icon that
turns into a check on success.

* fix(web): restyle system requirements as a spec rail

Replace the nested bullet card with mono-label / value pairs under the
installer section so it matches the craft language used elsewhere.

* fix(web): center engine rows and quiet the Default badge

Align name/badge with multi-line engine details on sm+; replace the
filled green pill with a small mono outline chip.
2026-07-24 01:15:45 -07:00
Jatin K MalikandGitHub 8c4f743e82 fix(ui): first-run dialog response without Gtk.Dialog.do_response (#580)
* fix(ui): use Gtk response signal for first-run dialog

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

Fixes #566

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

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

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

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

* test(ui): cover _on_response via SimpleNamespace

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

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

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

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

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

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

SimpleNamespace calls against vocalinux.ui.first_run_dialog.FirstRunDialog
fail under the full suite when earlier tests leave the class as a MagicMock.
Reload the source under a unique module name with a real Gtk.Dialog stand-in
(as in 680c5ed) so _on_response and connect("response") stay green on CI.
2026-07-24 00:44:47 -07:00
Jatin K MalikandGitHub a507b16c89 fix(aur): move clipboard and wtype tools to optdepends (#586)
Demote xclip, wl-clipboard, and wtype from hard depends to optdepends.
These are injection/clipboard fallbacks (alongside existing xdotool);
IBus and other primary paths may not need them on every install.

Responds to AUR feedback (Ataraxy, comment #1079450). Follow-up to #579
(virtual python-pywhispercpp); AUR picks this up on the next v* publish.
2026-07-24 00:03:19 -07:00
8a923151cc fix(aur): depend on virtual python-pywhispercpp instead of -cpu (#579)
The PKGBUILD hard-depends on python-pywhispercpp-cpu by name while
optdepends tells GPU users to "replace" it with the -cuda variant.
That swap is impossible in a normal pacman transaction: the -cuda
package conflicts with -cpu but only provides the virtual
python-pywhispercpp name, so removing -cpu breaks vocalinux's
dependency and pacman aborts. The only workaround is pacman -Rdd,
which leaves a broken dependency record that re-breaks on every
vocalinux upgrade.

All three AUR backend packages (-cpu, -cuda, -rocm) declare
provides=('python-pywhispercpp'), so depending on the virtual name
lets any variant satisfy the dependency. AUR helpers prompt for a
provider on fresh installs, the standard Arch pattern for
swappable backends.


Claude-Session: https://claude.ai/code/session_01XAF9BkcSzwcMjTzkBxdx6k

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 23:49:29 -07:00
Jatin K MalikandGitHub ac59a681ac fix(install): prefer both libgirepository 1.0 and 2.0 when present (#583)
On Ubuntu 24.04+ apt still lists libgirepository1.0-dev, so the installer
kept only 1.0 and skipped 2.0. Pip builds of PyGObject need the modern GI
headers from 2.0; install both packages when both exist.

Fixes #571
2026-07-23 23:43:17 -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 02b341bae3 feat(packaging): add self-contained AppImage build (x86_64 + aarch64) (#573)
* feat(packaging): add self-contained AppImage build

Bundles the wheel plus PyGObject/GTK3/AppIndicator/IBus typelibs into a
relocatable x86_64 AppImage via linuxdeploy + linuxdeploy-plugin-gtk, so
users get a zero-install, no-sandbox-negotiation alternative alongside
AUR/PyPI. Wired into nightly and tagged releases, plus a smoke-test job
in the PR pipeline since AppImage/ELF bundling can't be verified on a
macOS dev machine.

* feat(packaging): add aarch64 AppImage builds

Parameterizes build.sh on uname -m instead of hardcoding x86_64 (both
linuxdeploy and appimagetool publish aarch64 continuous builds, and
pywhispercpp ships manylinux_aarch64 wheels, so no from-source compile
needed). Adds an arch matrix to the PR smoke-test job, and a second
job in nightly.yml/release.yml that builds on GitHub's free
ubuntu-24.04-arm hosted runner and attaches the aarch64 AppImage to
the same release the x86_64 job already created.
2026-07-23 23:05:05 -07:00
Anay GarodiaandGitHub 513e4749e3 fix(xdotool): preserve input focus after injection (#564) 2026-07-23 21:44:49 -07:00
89f2227478 fix(uninstall): remove ~/.local/bin launcher wrappers reliably (#569)
uninstall.sh only removed ~/.local/bin/vocalinux when it was a symlink
([ -L ]), but a repo/dev install writes a real file there, and
~/.local/bin/vocalinux-gui was never removed — both were left behind.

- Remove both wrappers via safe_remove in remove_application_files (handles
  files and symlinks).
- safe_remove also handles dangling symlinks (-e follows links and is false
  for a dead link, so also test -L).
- verify_uninstallation checks both wrappers.
- Drop the redundant symlink-only block in remove_curl_install_files.

Co-authored-by: Alexander <ab@digital-benefits.de>
2026-07-23 21:43:12 -07:00
Anay GarodiaandGitHub fbcd28d577 fix(vosk): add missing Italian and English-India entries to medium/large model tables (#551)
Fixes #550.

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

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

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

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

Fix:
- Add the missing "it"/"en-in" entries to the medium/large tables so all
  10 supported languages now resolve consistently across all three sizes.
- Add a guard in _get_vosk_model_path() that raises a clear ValueError
  ("No VOSK '<size>' model available for language '<lang>'...") instead of
  crashing deep inside os.path.join, so any future data gap fails loudly
  and helpfully rather than with a cryptic TypeError.
2026-07-19 22:53:07 -07:00
Jatin K MalikandGitHub 830d24577a docs(readme): redistribute header badges into three rows (#545)
Four badge rows felt too tall above the hero. Collapse to three
rows (5 / 6 / 5) while keeping the narrow-to-wide step-out layout.
2026-07-17 19:35:30 -07:00
Jatin K MalikandGitHub d010a16eda docs(readme): pin CI badge to main and restyle social/coverage badges (#544)
* docs(readme): pin CI badge to main and restyle social/coverage badges

Scope the Vocalinux CI status badge to the main branch so PR failures no
longer flip the README shield. Restyle the X follow badge to black/white X
aesthetics, and set Codecov coverage.range so 80%+ coverage shows green.

* docs(readme): use static X follow badge without broken count

Shields.io no longer returns Twitter/X follower counts (empty right-side
value after API restrictions). Switch to a static black badge that shows
Follow @jatinkrmalik with the X logo instead of an empty dynamic count.
2026-07-17 19:09:04 -07:00
Jatin K MalikandGitHub 10b6f479a8 docs(readme): center-align header before hero image (#542)
* docs(readme): center-align header before hero image

Wrap icon, title, subtitle, and shields in a centered div so the top
of the README lines up cleanly above the hero image.

* docs(readme): tighten header badges and add social/install shields

Three clear rows: identity (Stable, release, license, PyPI, AUR),
quality (CI, Python, Linux, codecov), and community (stars, forks,
issues, contributions, X follow). Drop low-signal noise (watchers,
commit activity, last commit, Made with GTK). Slightly larger logo.

* docs(readme): add privacy, distro, and PRs-welcome shields

Expand the header into five scannable rows: identity, product values
(privacy/offline, data stays local, X11|Wayland, PRs welcome), official
distro logos linking to DISTRO_COMPATIBILITY, quality, and community.
Drop the generic Platform: Linux badge in favor of named distros.

* docs(readme): trim header to four badge rows

Drop Stable (release version is enough), merge privacy into one badge,
and remove stars/forks/issues (already on the GitHub repo page). Keep
X follow with CI/Python/codecov on the last row.

* docs(readme): order badge rows narrowest to widest

Step the header out toward the full-width hero: values → identity →
quality/social → distros as the base row.
2026-07-17 18:58:14 -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 d8f15d83d3 fix(ui): polish settings tab scrolling after #538 (#541)
* fix(ui): polish settings tab scrolling after oversized-dialog fix

Forward wheel events from unfocused combos/spins to the tab scroller,
drop the nested Advanced ScrolledWindow, and clear tab scroller shadows.

* refactor(ui): re-dispatch scroll to parent ScrolledWindow

Drop hand-rolled vadjustment math; forward the event and let GTK scroll.
2026-07-17 18:24:20 -07:00
e2ba077e95 fix(ui): make settings tabs scrollable so dialog fits the monitor (#538)
The settings dialog opened taller than the screen (spilling onto a
second monitor) because none of the notebook tabs scrolled. A
Gtk.Notebook requests the minimum size of its tallest page, and the
Speech Engine (~950px) and Recognition (~1080px) tabs pushed the
window's minimum height to ~1350px at 700px width, overriding
set_default_size() entirely on 1080p displays.

Wrap each tab in a vertical-only Gtk.ScrolledWindow so the notebook's
minimum height collapses and the default size takes effect, and raise
the default height from min(600, 40% of screen) to min(800, 75%) since
tall content now scrolls instead of stretching the window.

Measured before/after on a 1920x1080 monitor: opens at 700x1351 before,
700x800 after (window minimum drops from 684x1161 to 684x243).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 18:14:34 -07:00
Jatin K MalikandGitHub 9e9f197a7b fix(ibus): wait for FocusIn before commit on scoped injection (#533)
* fix(ibus): wait for FocusIn before commit on scoped injection

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

Fixes #523

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

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

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

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

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

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

* test(ibus): cover start_engine_process failure diagnostics

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

* chore: re-trigger CI after rebase onto main
2026-07-16 22:38:32 -07:00
Jatin K MalikandGitHub 10db2ba93f ci(flatpak): build arch matrix without job-level matrix if (#536)
GitHub rejects job-level if expressions that reference matrix
(Unrecognized named-value: matrix). Resolve the include list in a
small setup job instead so PRs stay x86_64-only.
2026-07-16 20:18:10 -07:00
Jatin K MalikandGitHub dea8e68544 ci(flatpak): reuse builder cache and skip aarch64 on PRs (#535)
Using github.sha in the cache key forced a full rebuild on every commit.
Hash packaging/flatpak instead so .flatpak-builder can restore across
runs, and only rebuild modules that actually changed.

PRs build x86_64 only; main and workflow_dispatch still cover both arches.
Drop the extra upload-artifact step in favor of the action's own upload.
2026-07-16 19:58:19 -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 89a28ef573 docs(web): add Flathub domain verification well-known file (#530)
Empty org.flathub.VerifiedApps.txt under /.well-known/ so Flathub can
verify com.vocalinux.Vocalinux against vocalinux.com after GitHub Pages deploy.
2026-07-16 18:44:43 -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
48a4dd6374 fix: sg not found on Ubuntu 26.04/Debian 13 (#524)
* fix: sg not found on Ubuntu 26.04/Debian 13

The 'sg' command moved from the 'login' package to 'util-linux-extra'
in Debian 13 and Ubuntu 26.04, which is not installed by default.
This causes vocalinux and vocalinux-gui wrappers to fail with
'exec: sg: not found'.

Two changes:
1. Add util-linux-extra to APT dependency lists (Ubuntu + Debian 13+)
2. Add command -v sg safety check in both wrapper scripts so they
   fall back to direct execution if sg isn't available

Fixes #522

* fix: attempt to install util-linux-extra if sg is missing

When the wrapper scripts detect that sg is not available, they now
attempt to install util-linux-extra via apt-get before falling back
to direct execution. This provides a better user experience on
Ubuntu 26.04/Debian 13 where sg moved to a separate package.

The installation runs silently and only triggers if:
1. User is in input group but session doesn't have it
2. sg command is not found
3. apt-get is available (Debian/Ubuntu systems)

If installation fails or sg still isn't available, the scripts
fall back to direct execution (app starts but keyboard shortcuts
won't work until next login).

* refactor: extract shared sg check, drop runtime install

- Extract sg fallback logic to shared SG_CHECK variable (eliminates duplication)
- Remove runtime apt-get install of util-linux-extra (dependency list handles it)
- Flatten to simple command -v sg check with direct exec fallback

net: -24 lines

---------

Co-authored-by: Alfred Pennyworth <alfred@openclaw.ai>
2026-07-15 21:00:34 -07:00
Jatin K MalikandGitHub eba4801043 docs(screenshots): refresh v0.14 UI shots and add website gallery (#521)
* docs(screenshots): refresh v0.14 settings/About shots and add website gallery

Replace the outdated About dialog screenshot and supersede the old single
settings dialog with the six v0.14.0-beta settings tabs. Keep unique product
shots that have no replacements. Add a dedicated /screenshots/ website page
with header/nav discoverability and a homepage link (no full gallery on home).

* docs(screenshots): drop obsolete features-overview shot

The annotated features overview was the pre-tabbed settings panel and no
longer matches the v0.14 UI. Remove it from README, website gallery, and assets.

* docs(screenshots): ship full-resolution web transcription shot

Restore the original PNG instead of the downscaled copy so the website
gallery matches the repo assets bit-for-bit.

* feat(web): add click-to-expand lightbox on screenshots gallery

Open a full-screen dialog when a shot is clicked, with prev/next controls,
keyboard arrows, Escape to close, and captioned counter for browsing the set.

* fix(web): avoid next/image HMR crash on screenshots gallery

Use plain img tags in the client gallery so dev-mode webpack chunk
reloads do not throw options.factory call errors while hydrating.
2026-07-15 00:18:26 -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 0a7acaea2f fix(hotkeys): layout-aware combo keys for non-US layouts (#513) (#514)
* fix(hotkeys): resolve combo letters via active XKB layout

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

Fixes #513

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

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

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

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

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

Raise patch coverage for layout_key_map / evdev_code_for_key so codecov
patch target (80%) is met.
2026-07-14 19:02:26 -07:00
Jatin K MalikandGitHub b9eaae8c29 fix(web): resolve Dependabot npm alerts in package-lock (#515)
Bump brace-expansion, js-yaml, @babel/core, and related transitive
deps via npm audit fix. Override nested postcss so Next.js no longer
pulls vulnerable 8.4.31 (XSS in CSS stringify output).
2026-07-14 19:01:18 -07:00
Jatin K MalikandGitHub 1441be0512 fix(text-injection): treat XIM none as unset (#512) 2026-07-14 18:50:28 -07:00
Jatin K MalikandGitHub 2fd7e7a7b3 Update README.md 2026-07-14 00:53:57 -07:00
Jatin K MalikandGitHub c942e9dd4a Enhance README with Star History section
Added a responsive star history chart to the README.
2026-07-14 00:53:03 -07:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fefc6962d8 chore(deps-dev): bump ws (#510)
Bumps the npm_and_yarn group with 1 update in the /web directory: [ws](https://github.com/websockets/ws).


Updates `ws` from 8.19.0 to 8.21.0
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.19.0...8.21.0)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 8.21.0
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 00:21:08 -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 f456160c45 fix(ui): keep preset and custom shortcut selection exclusive (#509)
Selecting a preset after recording a custom combo left the custom entry
filled, so both looked active. Sync the dropdown to "Custom Shortcut"
when a non-preset is set, and clear the custom field when a preset wins.
2026-07-13 18:53:00 -07:00
Jatin K MalikandGitHub 872a745bc4 fix(text-injection): restore GNOME Wayland IBus with bare xkb engine (#506)
#502 only exempted KDE from the #478 real-engine gate. GNOME/Mutter has
the same scoped-activation path, so stock sessions with only an xkb:*
layout fell through to wtype, which Mutter rejects, then clipboard-only.

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

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

* style(asr): apply black formatting

* test(main): include remote API model kwarg

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

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

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

* feat(asr): support FunASR remote responses

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

Drop chat-completions/Qwen client, evaluation notes, and the remote ASR
benchmark harness. Keep model field, tolerant OpenAI response parsing,
SenseVoice label stripping, FunASR docs, and tests.
2026-07-13 15:03:19 -07:00
Jatin K MalikandGitHub 9d9638e854 style(whispercpp): drop long comment on default thread count (#505)
Per review on #492, the rationale lives in the PR/history via git blame.
2026-07-13 14:48:42 -07:00
6c8aae86c0 fix(whispercpp): don't default to all CPU cores on hybrid processors (#492)
The CPU-only auto thread count was `min(cpu_count(), 8)`. On an 8-core
hybrid CPU (e.g. Intel Core Ultra, P+E cores) this selects 8 = every
core. whisper.cpp synchronises all worker threads at each decode step,
so pulling the slow efficiency cores into the pool makes every barrier
wait on the slowest core, and leaves nothing for the OS / mic capture.

Measured on a Core Ultra 7 355 (8 cores, no SMT), base model, 6s clip:

  n_threads=6 -> RTF 0.14x
  n_threads=8 -> RTF 2.13x   (~15x slower, i.e. slower than real time)

Default to `max(1, min(cpu_count() - 2, 8))`, which leaves headroom and
picks 6 on that machine (optimal in a sweep) while staying <= the
previous value everywhere else. Users can still override via the
`whispercpp_n_threads` config setting.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
2026-07-13 14:44:49 -07:00
b3605bf3c6 feat(shortcuts): configurable modifier+key hotkeys (e.g. Alt+R) (#493)
* feat(shortcuts): configurable modifier+key hotkeys (e.g. Alt+R)

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

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

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

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

Addresses Codex P2 review on PR #493.

* style: apply black formatting to configurable-hotkeys files

* test: cover _gdk_keyname_to_token shortcut-capture mapping

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

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

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

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

---------

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

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

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

* style: black-format injection modifier-wait test

* test: cover _held_modifier_keycodes evdev scanning (patch coverage)

* test: cover error paths in _held_modifier_keycodes

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

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

Addresses maintainer review on PR #494.
2026-07-13 14:40:04 -07:00