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
* 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(packaging): build pywhispercpp at the portable x86-64 baseline
Every packaged channel shipped a libggml-cpu compiled for AVX2+FMA:
the manylinux PyPI wheel itself carries -mavx2 -mfma -mbmi2 code, so
any package that bundles it dies with SIGILL when whisper.cpp loads a
model on a pre-Haswell CPU (the reporter's i5-3337U). GGML_NATIVE=OFF
alone is not the portable baseline either; while the native default
is on, ggml turns every GGML_* SIMD option on, so each is pinned off.
- Snap: force the sdist (PIP_NO_BINARY) with the flag floor via
CMAKE_ARGS in the part's build-environment.
- Flatpak: extend the module's CMAKE_ARGS with the same floor.
- Native .deb/.rpm: the build still vendors the wheels, then rebuilds
libggml-cpu alone from the same hash-pinned sdist and swaps it into
the vendored copies, asserting no AVX/FMA survives; the container
image gains cmake, g++, make and binutils.
- AppImage: extract the pinned-sdist reinstall into a shared helper
and route every CPU-only fallback of the Vulkan path through a new
baseline rebuild at the same floor; the Vulkan compile keeps its
own flags via the same helper. Keeping the wheel on those paths was
the bug's exact binary.
Fixes#957
* style(tests): apply black/isort to the new packaging guards
* fix(native): keep the wheel's libggml providers, alias its base/gomp sonames
Swapping the whole libggml set for the baseline build broke the import
smoke: the wheel's auditwheel-mangled core lib doubles as the
extension's __cxa_call_terminate provider, and its NEEDED entries are
the mangled names. Swap only libggml-cpu, and satisfy the baseline
library's own unmangled NEEDEDs (libggml-base.so.0, libgomp.so.1) with
aliases of the vendored copies, which carry no generated SIMD and are
the same upstream version. Fail closed when the vendored set has
restructured instead of silently shipping the AVX2 wheel.
* fix(packaging): fail builds that lose the speech engine, unblock the ISA assert
Greptile findings on the baseline work:
- A failed baseline rebuild in the AppImage returned success after
remove_appdir_pywhispercpp, so the bundle would ship with no speech
engine at all while logging that the wheel was kept. Propagate the
failure through every CPU-only fallback so the build stops instead.
- The native libggml-cpu assertion used grep -q under pipefail, where
grep's early exit SIGPIPEs objdump and the pipeline reports a miss,
letting an AVX2 library through. Read the full disassembly instead.
* fix(installer): build pywhispercpp from sdist on the CPU path
install_cpu_pywhispercpp let pip pick the manylinux wheel, whose
libggml-cpu is compiled for AVX2+FMA and SIGILLs on pre-Haswell CPUs
(#957). Pass --no-binary pywhispercpp like the Vulkan/CUDA paths so the
sdist compiles against this machine's own instruction set.
* fix(installer): install C++ toolchain on distros that ship without one
Forcing the pywhispercpp sdist on the CPU path needs a compiler, and the
fedora, mageia, alpine, void and solus package maps did not request one.
Add gcc-c++/make (rpm), build-base (alpine), base-devel (void) and
gcc/g++/make (solus) so source installs compile on minimal systems.
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* fix(installer): keep piped script on stdin and prompt via /dev/tty
`curl | bash` left bash reading the installer itself from stdin, and the
detection block's `exec < /dev/tty` (from #315) closed that pipe about 300
lines into a 3000-line script. curl died with error 23 writing to the
closed pipe, and bash read the rest of the installer from the terminal as
commands instead of finishing the script.
stdin now stays on the pipe and a reachable terminal is recorded in
TERMINAL_PROMPTS. Every interactive prompt goes through read_prompt, which
reads /dev/tty explicitly only for that read, so user input still works in
a terminal while the script body keeps flowing through the pipe. The
interactive-mode gate now checks TERMINAL_PROMPTS rather than `-t 0`,
which also lets `--interactive` work under a pipe.
Regression coverage: the remote-install gate's second run now pipes the
bootstrap in under a `script` pty (the literal `cat | bash` shape a
curl user takes), and new tests run install.sh in a pty with the script on
a pipe to assert the mode and engine prompts still answer on the terminal.
Fixes#933
* fix(installer): drain the piped bootstrap before the handoff exec
A curl|bash bootstrap leaves its unread tail on stdin; the tagged installer
reads its commands from a file, so nothing drained the pipe and a still-
streaming curl reported error 23 when the installer exited. Drain stdin to
EOF before exec (Greptile P2 on #935).
Gate: drip-feed the piped run so the producer is mid-write at the handoff
like real curl, and run it under pipefail so a producer killed early fails
the run. Tests: pass --tag so a slow release lookup cannot eat the window,
wait for the engine prompt before Ctrl+C, and quote the installer path.
* test(installer): propagate a killed producer's status in the piped gate
A cat dying mid-drip (SIGPIPE when the consumer abandons the pipe) was
swallowed by the loop and the final rm, so drip returned success and
pipefail could not see the download failure the arm exists to catch.
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
The pinned pycairo builds from source everywhere, and the sdist needs
cairo.pc. Fedora, mageia and the debian/ubuntu maps list the headers
explicitly; suse relied on gobject-introspection-devel pulling
pkgconfig(cairo) transitively, which the current Tumbleweed image no
longer does, so the opensuse/tumbleweed install gate now fails at
"Dependency cairo not found" during pycairo metadata generation.
void, solus and alpine have the same latent gap; each gets the same
package spelled in its own convention.
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* fix(hotkeys): consume the dictation shortcut so it stops leaking to apps
Grab each monitored keyboard with EVIOCGRAB and re-emit every event the
shortcut does not own through a paired uinput clone. The configured
shortcut key is now consumed by Vocalinux instead of also reaching the
focused app (right-alt opening menus in Thunderbird/Firefox, F11/F12
triggering browser actions).
- Pure-modifier gestures reserve the modifier's events entirely; bare
function-key shortcuts consume that key; modifier+key combos consume
the main key only while the combo owns it, so typing and chords like
Alt+Tab keep working and injected text is untouched
- BUS_VIRTUAL devices (our clones, ydotool, remappers) are never grabbed
- EV_REP is stripped from clones and repeats are forwarded verbatim, so
repeat rate is preserved without doubling
- Feedback event types (EV_LED/EV_SND/EV_FF) are never re-injected
- Falls back to ungrabbed listening when /dev/uinput or the grab is
unavailable, warning once that keys will also reach the app
Fixes#871
* test(hotkeys): cover grab/forward edge cases
Race cleanup on duplicate open, forwarder close errors during stop and
device removal, mid-SYN_DROPPED forwarding, write failures, and the
combo consume edge cases (unowned repeat/release, missing main key).
* fix(installer): grant read on /dev/uinput so key suppression works
The evdev backend's uinput clone opens /dev/uinput O_RDWR, but the
installed 80-dotool.rules gave the input group write-only access
(0620), so shortcut suppression silently fell back to ungrabbed
monitoring on stock installs. Bump the rule to 0660 and refresh it when
the installed copy differs so existing installs pick up the fix.
* fix(hotkeys): harden evdev grab/forward lifecycle and udev install
- release the device grab and close the clone when a uinput write fails
- close all devices if the monitor exits unexpectedly (releases grabs)
- monitor virtual devices too; skip only our own uinput clones
- track consumed combo presses per device so releases stay paired
- after SYN_DROPPED, release keys the clone still believes are held
- append the uinput udev rule instead of overwriting local rules
- annotate new signatures and narrow the clone-creation catch
* fix(hotkeys): reconcile swallowed combo releases after SYN_DROPPED
- resync the swallowed-press pairing even when the clone holds no keys;
a dropped combo release otherwise leaves the next ordinary press
consumed, eating a keystroke
- narrow every new close-cleanup catch to OSError/IOError/RuntimeError
with debug context instead of blanket Exception passes
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
* refactor(installer): split implementation into sourced modules
* test(installer): harden module path coverage
* fix(installer): qualify interactive privacy and engine claims
The guided installer overstated local-only privacy and ranked
whisper.cpp without evidence. Match PRODUCT.md: on-device local
engines, optional Remote API, and the ~33 language catalog plus
Auto-detect.
---------
Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>