* feat(tray): allow disabling missing tray warning dialog
Fixes#620
* test(tray): stabilize missing-watcher opt-out coverage
Default the D-Bus ListNames mock to include StatusNotifierWatcher so
TrayIndicator construction no longer opens the warning dialog during
setUp, and bind the don't-show-again checkbox test to the module Gtk mock.
* test(tray): make missing-watcher opt-out tests deterministic
* chore(release): prepare v0.15.0
Bump version across app, web, AUR, and Flatpak metainfo. Document GitHub
release-notes rules, refresh README/UPDATE/INSTALL/USER_GUIDE for the 0.15
series (sidebar settings, AppImage, auto-pause/keepalive, Vulkan device
selection), and align website product surfaces with shipped features.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* docs(release): fold latest main into v0.15.0 notes
Include dictation capitalization/trailing-space, ibus-wayland injection,
IBus teardown, and settings notice polish landed on main after the initial
release prep rebase.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* docs(release): include Bluetooth mic crash fix in v0.15.0 notes
Fold #599 (PortAudio Bluetooth SCO probe heap corruption) into README,
UPDATE, changelog, AppStream, and release history after rebasing onto main.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* docs(release): include language catalog expansion in v0.15.0 notes
Fold #616 (Hungarian and expanded speech languages, Fixes#565) into
README, UPDATE, changelog, AppStream, PRODUCT.md, and release history
after rebasing onto latest main.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* docs(release): fold settings footer and language follow-ups into v0.15.0
Include #618 sidebar dictation footer, #619 Custom Shortcut Record/Set
restore, and #617 English (India) Whisper language mapping after rebasing
onto latest main.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(settings): restore custom shortcut Record/Set controls
Gtk.Widget.show_all() is a no-op while no_show_all is True, so the
contextual Custom Shortcut row never appeared after selecting it in the
searchable settings UI. Clear the flag before showing and restore it on
hide.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(settings): sync custom row when reverting shortcut separators
Selecting Custom Shortcut then a combo group separator restored the
preset id but left Record/Set visible. Reuse _sync_shortcut_selection_ui
so combo and custom-row visibility stay aligned.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(settings): clear stale Record/Set hint on separator revert
Selecting Custom Shortcut sets a temporary info hint; clicking a combo
separator already restored the row visibility but left that hint in
place. Refresh the mode description after syncing.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(languages): map en-in to Whisper code en
English (India) used the catalog id as the Whisper language argument,
which is invalid. Resolve Whisper/whisper.cpp/remote_api codes via the
SUPPORTED_LANGUAGES whisper field so en-us and en-in both become en.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* test(languages): cover whisper language resolver fallbacks
Exercise unknown catalog ids and legacy en-us fallback paths so
codecov/patch covers resolve_whisper_language fully.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
Expose Hungarian and other high-demand Whisper languages in Settings/CLI,
wire official VOSK models where Alphacephei ships them, keep CLI choices
derived from SUPPORTED_LANGUAGES, and update the languages marketing page
with honest per-engine support.
Fixes#565
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(audio): prevent Bluetooth mic probe heap corruption
Open PortAudio streams only once when negotiating capture format, and
always stop_stream() before close(). Separate channel/rate probing was
aborting on Bluetooth SCO devices (e.g. FreeBuds) with malloc corruption.
Fixes#567
* refactor(audio): open capture stream once instead of probe-close-reopen
Adversarial review of the first fix found it still closed the probe
stream and immediately reopened the device — the same rapid open/close
pattern blamed for the Bluetooth SCO heap corruption, just fewer cycles.
_open_capture_stream() now returns the successfully negotiated stream
and all three callers (mic test, recording, reconnect) use it directly,
so each capture session performs exactly one PortAudio open. Also drops
the dead known_working_rate parameter.
* test(audio): cover negotiation fallback paths for codecov patch
Add tests for mic-test, recording, and reconnect fallback branches when
_open_capture_stream returns no stream, plus Bluetooth settle-on-failure
and empty-read reconnect cleanup. Brings patch diff coverage above 80%.
* feat(dictation): append trailing space after completed transcription
Fix cross-session glueing ("Hello.This") for push-to-talk and toggle
by appending a trailing space to each injected segment instead of relying
on in-session leading-space memory that is cleared on IDLE.
Adds an optional Settings toggle (default on) and preserves legacy
leading-space behavior when disabled. Newlines from voice commands are
left without a trailing space.
Fixes#605
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(dictation): honor trailing-space setting from disk live
Read append_trailing_space from config.json on each injection so the
Settings toggle applies without restart. main() and TrayIndicator each
own a ConfigManager, so an in-memory read missed Settings writes — same
pattern as TextInjector.copy_to_clipboard.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* test(dictation): align capitalize tests with trailing-space default
Update auto-capitalize expectations to include the default trailing
space, and add edge-case coverage for legacy mode, newlines, empty
input, and config-read failures so patch coverage clears 80%.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* feat: auto-capitalize sentences after punctuation
Add capitalize_sentences() function that uppercases the first letter of
dictated text and after sentence-ending punctuation (., !, ?) followed by
whitespace. Preserves URLs, decimals, and abbreviations without trailing space.
- Config option text_injection.auto_capitalize (default: true)
- Settings toggle in General tab to opt out
- 17 tests covering edge cases
Fixes#553
* style: black-format auto-capitalize helpers for CI lint
Fixes the Lint Python check failure on the auto-capitalize PR.
* refactor: make auto-capitalize Vosk-only with clear UI indication
- Gate auto-capitalize feature to Vosk engine only (Whisper outputs proper casing)
- Update settings tooltip to explain Vosk-only behavior
- Update subtitle to show '(Vosk only)' for user clarity
* test(main): cover Vosk-only auto-capitalize injection path
Add callback wiring tests for enabled/disabled auto-capitalize on Vosk
and the Whisper skip path so codecov patch coverage clears the 80% bar.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* refactor(settings): move Auto-Capitalize to Recognition tab
Auto-capitalize is dictation output post-processing, not application
lifecycle behavior, so it belongs alongside Voice Commands rather than
with autostart/clipboard settings in General.
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
Drop the thick left accent strip on .info-box helpers so they read as
muted flat surfaces with a thin full border, matching the preference
cards instead of looking like callout banners.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(ibus): keep engine teardown correct when parent destroy fails
`do_destroy` passed `super().do_destroy` to `_handle_engine_destroy`.
PyGObject binds that vfunc to the GType rather than to the instance, so
calling it with no arguments raises:
TypeError: IBus.Object.destroy() takes exactly 1 argument (0 given)
This fired on every engine teardown. The exception escaped
`_handle_engine_destroy` before its return, so neither
`_active_instance = ...` nor `_focus_event.clear()` in the caller ran,
leaving a destroyed engine registered as active with focus still set --
the two values `_ibus_ping_status` (#523) reads to decide commit
readiness.
Two changes:
- Bind the parent destroy to the instance, falling back to
`IBus.Object.destroy(self)` only when the no-arg call raises TypeError,
so this keeps working if PyGObject binds the vfunc correctly later.
- Guard the parent destroy call so no failure there can skip teardown.
Fixes#606
* refactor(ibus): extract parent-destroy workaround into a testable helper
Codecov flagged the new code as uncovered (36% patch, below the 80%
target). The whole `if IBUS_AVAILABLE:` branch of `do_destroy` is
unreachable from the suite, which only ever exercises that method with
IBUS_AVAILABLE False -- the single line it replaced was already
uncovered on main for the same reason.
Move the workaround into a module-level `_invoke_parent_destroy`, the
same pattern `_handle_engine_destroy` already uses to make `do_destroy`
testable, and bind it with functools.partial at the call site.
Behaviour is unchanged. Coverage of the branch goes from 0 to all but
the one-line partial, and three tests now pin the actual contract:
pass-through when the binding is correct, retry with the instance on the
GType-binding TypeError, and no masking of other exceptions.
* feat(text-injection): use IBus on unbridged compositors when ibus-wayland runs
The `_IBUS_UNBRIDGED_COMPOSITORS` denylist assumes nothing sits between
the compositor and ibus-daemon. Since IBus 1.5.32 that is no longer a
safe assumption: IBus ships `ibus-wayland`, which implements
`zwp_input_method_v2` and relays commits to native Wayland clients
speaking text-input-v3. Every compositor on the denylist exposes
`zwp_input_method_manager_v2`, so with the bridge running they are
bridged, and skipping IBus falls back to a slower path unnecessarily.
Add a live `pgrep -x ibus-wayland` probe and let it override the
denylist. Scoped to the denylist branch only, so GNOME and the KDE
VirtualKeyboard path (#574) are untouched.
Behaviour is unchanged when the bridge is not running, and because the
check is a live probe rather than configuration it degrades back to the
denylist by itself if the bridge dies or was never started. Cost is one
pgrep per TextInjector construction, not per injection.
Verified on Hyprland 0.56.1 with IBus 1.5.34: dictation into Electron
apps (VSCodium, Electron 39/41) and kitty commits through IBus, 303
chars in ~200ms versus 16.8s for 415 chars via `ydotool type`.
Also adds VOCALINUX_FORCE_BACKEND (ibus|wtype|ydotool|auto) as discussed
in #607. Autodetection has to infer whether IBus commits actually reach
the focused app and cannot verify it -- commit_text() reports success
even when the text is dropped -- so an escape hatch is useful when the
inference is wrong, and it makes the two paths A/B-testable without
editing code. Unknown values fall back to auto with a warning rather
than silently pinning a backend. Related to #476 and #402; happy to drop
this from the PR if it should follow their design instead.
Two existing tests asserted the unbridged outcome without pinning the
probe -- one blanket-mocks subprocess.run to returncode=0, the other
shells out for real -- so both now state the "no bridge" precondition
explicitly rather than depending on the host.
Note for users: Chromium only opens a zwp_text_input_v3 seat when
launched with --enable-wayland-ime; without it the bridge runs and the
app still receives nothing.
Closes#607
* test(text-injection): cover bridge override and forced-backend branches
Codecov flagged the new branches in _check_dependencies as uncovered
(74% patch, below the 80% target).
Adds four tests to TestCheckDependencies:
- unbridged compositor + ibus-wayland running selects IBus
- VOCALINUX_FORCE_BACKEND=wtype pins wtype where IBus would be chosen
- VOCALINUX_FORCE_BACKEND=ydotool pins ydotool even with wtype present
- VOCALINUX_FORCE_BACKEND=ibus bypasses the reachability guards, with
neither a daemon nor a bridge available
Every line added by this PR is now covered.
Fix ESLint errors for hash install links, anonymous default-export
warnings, and the ineffective next.config headers() that warned under
static export. Correct softwareVersion sync to match unquoted JS keys,
and bump Actions to Node-24 runtimes so deprecation notices stop.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
Remove Disallow for query-string URLs and /_next/, which caused Google
Search Console "Indexed, though blocked by robots.txt" for legitimate
pages discovered with UTM/tracking params. Canonical tags already handle
duplicate param URLs.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(ui): AppImage smoke fixes for tray icons, labels, and identity
Replace placeholder mic tray SVGs with opaque branded icons, show
whisper.cpp instead of Whisper_cpp, set GLib/AppIndicator identity to
Vocalinux (not main.py), and give About dialog buttons enough padding
so labels are not clipped under Adwaita.
* fix(packaging): harden AppImage build for Wayland and tray deps
Pin the build interpreter, install onnxruntime into the bundle, set
desktop Exec=AppRun + StartupWMClass, prune typelibs to an allowlist
after linuxdeploy, and stop forcing GDK_BACKEND=x11 on Wayland
(VOCALINUX_FORCE_X11=1 remains as an escape hatch). CI AppImage jobs
also pull Ayatana AppIndicator and librsvg SVG loader packages.
* fix(packaging): bundle full stdlib from base_prefix in AppImage
When build.sh runs under a venv, sys.prefix only has site-packages, so
the AppImage was missing encodings and crashed on start. Copy the
interpreter and stdlib from sys.base_prefix, force pip
--ignore-installed into AppDir, set Gdk program class to Vocalinux, and
tone down the muted tray slash so small StatusNotifier icons stay slate.
* fix(tray): use absolute icon paths so stale hicolor icons lose
XFCE StatusNotifier resolves IconName through the system theme and can
ignore IconThemePath. A leftover ~/.local/share/icons placeholder
(vocalinux-microphone-off.svg filled #e74c3c) was winning over the
AppImage's bundled icons. Outside Flatpak, pass absolute SVG paths to
set_icon_full / new_with_path instead of bare theme names.
* feat(settings): replace tab navigation with searchable sidebar and topic pages
- Sidebar (icon + label) with Gtk.Stack replaces the 7-tab notebook
- Live settings search (Ctrl+F): filters rows in place, per-page match
counts, dims pages without hits, empty-results state
- Topic-based pages per GNOME HIG: Dictation, Speech Model, Audio,
Performance, Application, Advanced
- Persistent bottom status strip: recognition state, shared mic level
bar, and Test Dictation with inline transcription output
- GPU device selection moves out of the power-user gate onto the
Performance page; Reset to Defaults moves into the Advanced page
- Consolidated model info card (info + warning + legend in one surface)
- Contextual custom-shortcut row, in-list auto-pause empty state,
uniform control widths, aligned group descriptions
- Esc clears search, Ctrl+W closes
* test(settings): update source-inspection tests for sidebar redesign, add search coverage
* fix(settings): index unlocked advanced controls in search
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* fix(settings): restore selected page after clearing search
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
* 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>
* 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>
* 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.
* 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>
* 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>
Anay GarodiaGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* 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>
* 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.
* 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.
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.
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>
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
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
* 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.
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>
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.
* 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.
* 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.
* 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.
* 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.
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>
* 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.
* 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
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.
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.