Rust rebuild viability #2

Closed
opened 2026-11-05 15:23:02 +00:00 by xavierk · 1 comment
Owner

Part of #1

Question

Is a Rust rebuild of the Vocalinux tray dictation app sound on Void Linux Wayland, or should Voculinux stay on Python? Compare capture (PipeWire), whisper.cpp inference, text injection, StatusNotifier tray, and GTK UI parity, plus rebuild cost and xbps packaging effort.

Part of #1 ## Question Is a Rust rebuild of the Vocalinux tray dictation app sound on Void Linux Wayland, or should Voculinux stay on Python? Compare capture (PipeWire), whisper.cpp inference, text injection, StatusNotifier tray, and GTK UI parity, plus rebuild cost and xbps packaging effort.
xavierk added the wayfinder:research label 2026-11-05 15:23:02 +00:00
Author
Owner

Rust rebuild viability — Vocalinux on Void Linux (Wayland)

  • Ticket: Gitea "Rust rebuild viability", map "Voculinux port"
  • Question: Is a Rust rebuild of the Vocalinux Python GTK3 tray dictation
    app sound on Void Linux Wayland, or stay on Python?
  • Method: local checkout (src/vocalinux/, v0.18.5) read directly;
    external claims traced to primary sources (official docs, protocol
    XML, crate docs/source, Void void-packages source). Secondary
    write-ups were used only as search leads, never as citations.
  • Date of local measurement: this checkout, commands quoted in §7.

Verdict

Stay on Python. Do not do a wholesale Rust rebuild now.

A Rust rebuild is technically feasible area by area — PipeWire,
whisper.cpp, tray, even GTK have real Rust bindings — but it is not
sound as a rewrite, for three load-bearing reasons:

  1. The hard problems are not language problems. Wayland text
    injection and global hotkeys are limited by compositor protocols
    and portal consent, identically in Rust and Python (§3). Rust buys
    nothing there, and the current app's value is precisely its
    multi-backend fallback ladder (IBus, RemoteDesktop portal, wtype,
    ydotool/uinput, xdotool/XWayland, clipboard paste).
  2. Rust forces a simultaneous GTK3 → GTK4 migration. The Rust
    GTK3 bindings are unmaintained and dropped upstream (§5), so
    "port to Rust" secretly means "port language and toolkit and
    tray model at once" — three rewrites stacked, each able to break
    parity alone.
  3. Rebuild cost dwarfs plausible gain. The app is ~48k LOC of
    Python with ~88k LOC of tests (§7). Inference already runs in
    native whisper.cpp via pywhispercpp; Python is orchestration
    around C/C++ engines, PortAudio, and D-Bus. Rust would mainly
    speed up code that is not the bottleneck, while discarding the
    test suite that encodes years of compositor/device edge cases.

Sound Rust use, if desired: an incremental native sidecar or
prototype (PipeWire capture + whisper.cpp core behind the existing
Python UI, or a small tray-less dictation daemon) to prove
injection on the actual Void compositors targeted — not a flag-day
rewrite. See §9.

0. What exists today (local, primary)

Fact Evidence
Python ≥3.11, GTK 3 via PyGObject (distro python3-gi, never pip) pyproject.toml (requires-python, deps); AGENTS.md toolchain section
Default engine whisper.cpp via pywhispercpp>=1.5,<2; optional OpenAI Whisper, Vosk, faster-whisper, Parakeet (sherpa-onnx), remote API pyproject.toml [project.optional-dependencies]; src/vocalinux/speech_recognition/recognition_manager.py (5,691 lines)
Mic capture via PyAudio/PortAudio; system-audio via PipeWire pw-record/pw-dump subprocesses src/vocalinux/audio/capture.py (1,207 lines), src/vocalinux/audio/pipewire.py (572 lines)
Silero VAD via onnxruntime, bundled silero_vad.onnx src/vocalinux/speech_recognition/silero_vad.py, data/
Injection backends: IBus engine, XDG RemoteDesktop portal, wtype, ydotool, xdotool, clipboard src/vocalinux/text_injection/text_injector.py (3,489), ibus_engine.py (1,882), remote_desktop_portal.py (788)
Tray via AyatanaAppIndicatorGlib 2.0 → AyatanaAppIndicator3 → AppIndicator3 fallback chain src/vocalinux/ui/tray_indicator.py:28-55 (1,983 lines), main.py:272-288
Hotkeys: pynput (X11), evdev (Wayland) src/vocalinux/ui/keyboard_backends/{pynput_backend,evdev_backend}.py
GtkLayerShell overlay, libnotify, logind suspend, single-instance lock, D-Bus service ui/dictation_overlay.py, ui/notifications.py, suspend_handler.py, single_instance.py, dbus_service.py
Void already a supported install target scripts/distro-package-map.yaml:177-183 (python3-gobject, gtk+3, libappindicator-gtk3, portaudio-devel, xdotool, wtype, ydotool…), docs/DISTRO_COMPATIBILITY.md:196, install.d/ xbps branches
Size: 47,976 LOC Python (84 files), 87,649 LOC tests (155 files) wc -l on src/vocalinux/**/*.py, tests/test_*.py (§7)

Note the Void-specific wrinkle already present in Python: the
ydotool guidance in text_injector.py:57-67 is systemd-centric,
while Void uses runit — language choice does not fix service
supervision either.

1. PipeWire capture — Rust: viable, arguably better

Assessment: native pipewire crate > today's pw-record subprocess

  • pw-dump JSON parsing (audio/pipewire.py) and > PortAudio device
    negotiation pain documented at length in audio/capture.py
    (format/channel aborts, Bluetooth SCO heap corruption comments).
    But: the crate is pre-1.0, callback/SPA pod construction is
    verbose unsafe-adjacent work, and device-hotplug/format edge cases
    would need re-earning the hard way. Net: genuine Rust win,
    medium effort, low risk.
    This is the strongest pro-Rust area.

2. whisper.cpp inference — Rust: viable, parity likely

Assessment: same underlying C++ engine as pywhispercpp, so
transcription quality/speed should match; GPU backends (Vulkan etc.)
become Cargo features/CMake flags instead of today's installer
rebuild dance (AGENTS.md, install.sh PYWHISPERCPP_VERSION).
Costs: long native builds, -sys build needs cmake + C++ toolchain
at package-build time (matters for xbps, §8), and re-implementing
the 5.7k-line recognition_manager.py logic (VAD segmentation,
engine dispatch, model download/checksum pinning, keepalive/unload,
diarization via tinydiarize in diarization.py, GPU probing
workarounds for old pywhispercpp versions). Optional engines
(Vosk/Parakeet/faster-whisper/remote) each need their own Rust
story (sherpa-onnx has a C API; Vosk has a Rust binding; none is
free). Net: feasible, no quality gain, high porting effort in the
orchestration layer around the engine.

3. Wayland text injection — Rust: no advantage; the binding constraint

This is the make-or-break area, and it is compositor-bound, not
language-bound. There is still no stable, universal "type this
string into the focused app" Wayland protocol:

Rust crates exist for the pieces (wayland-client,
wayland-protocols crates: https://docs.rs/wayland-protocols/latest/wayland_protocols/ ,
zbus/ashpd for portals/IBus-over-D-Bus), but every compositor
quirk, probe, and fallback currently encoded in ~6k lines of
text_injection/ + focused_window.py would have to be re-derived
and re-tested per compositor (wlroots/Sway, KWin, Mutter at
minimum). Net: parity is achievable in principle, most expensive
to re-prove, zero capability gain. Any Rust plan that starts with
UI or inference and leaves injection for later has the risk order
backwards.

Global hotkeys have the same shape: no general Wayland global-key
grab; today's evdev backend (/dev/input, permissions) maps to
Rust evdev crate (https://docs.rs/evdev/latest/evdev/) one-for-one,
with the same privilege caveats.

4. StatusNotifier tray — Rust: viable, small

Assessment: ksni is a clean replacement and drops the
Ayatana/AppIndicator typelib fallback chain entirely. Host support
is unchanged: needs a StatusNotifierHost (KDE, Waybar/wlroots
panels, XFCE; GNOME Shell needs the AppIndicator extension) — on a
typical Void Wayland setup (Sway/Waybar, Wayfire, labwc) that is
normally present. Icon-theme/pixmap, menu, and tooltip semantics
need care but are bounded. Net: genuine simplification, low
effort.
Second-strongest pro-Rust area.

5. GTK UI parity — Rust: forces GTK4; largest hidden cost

Assessment: there is no "same UI, new language" option. The
settings UI alone is ui/settings_dialog.py at 10,137 lines, plus
tray, overlay, dictation pad, first-run, transcript, logging
dialogs — ~25k lines under ui/ total. Rewriting that in gtk4-rs
means re-laying-out every dialog, re-doing GObject property/signal
wiring from idiomatic Python into Rust's subclassing/builder style,
and losing PyGObject's exact dynamic behaviours the tests pin down.
GTK4 parity is possible (gtk4-rs is actively maintained), but it
is a full UI rewrite, and GTK4-on-Wayland behavioural differences
(popovers, layer-shell overlay, global shortcuts absence) land at
the same time as the language change. Net: feasible only as a
GTK4 migration project; if that migration is ever wanted, doing it
in Python/PyGObject first (GTK4 is available to Python today) is
the lower-risk sequence.

6. Comparison summary

Area Rust today (primary source) vs current Python Effort / risk
PipeWire capture pipewire crate 0.10, freedesktop-hosted [§1] Better than pw-record subprocess; device graph native Med / Low
whisper.cpp whisper-rs 0.16 over whisper.cpp [§2] Same engine as pywhispercpp; no quality gain High (orchestration) / Med
VAD (Silero) ort ONNX bindings [§2] Parity Low / Low
Wayland injection protocols unstable/compositor-gated; portal keysyms [§3] No gain; fallback ladder must be rebuilt High / High
Global hotkey evdev crate = today's evdev backend [§3] Parity, same perms Low / Med
Tray (SNI) ksni 0.3 on zbus [§4] Simpler than Ayatana chain Low / Low
UI toolkit gtk3-rs dead; gtk4-rs maintained [§5] Forces GTK4 rewrite of ~25k LOC UI Very high / High
D-Bus/portals/notify/logind zbus, ashpd Parity with Gio/GLib usage Med / Med
xbps packaging build_style=cargo, --locked --offline [§8] Native -sys deps complicate; Python path already Void-tested Med–High / Med

7. Rebuild cost (measured, not imagined)

Local measurement on this checkout:

src/vocalinux/**/*.py   47,976 lines, 84 files
tests/test_*.py         87,649 lines, 155 files
  recognition_manager.py 5,691   settings_dialog.py 10,137
  text_injector.py       3,489   tray_indicator.py   1,983
  ibus_engine.py         1,882   capture.py          1,207

Inference (estimate, labelled as such): a like-for-like Rust port
is not a transliteration — GObject/GTK, asyncio-free threading,
and subprocess backends all change shape. At typical rewrite rates
for systems/desktop code with hardware-in-the-loop testing, parity
is a multi-person-month project (order 6–12+ months for one
developer to reach tested parity across compositors and audio
devices), during which the Python app either freezes or the port
chases a moving target (v0.18.5 and releasing regularly per
docs/RELEASE_PROCESS.md). The 88k-line test suite — which pins
exactly the edge cases a rewrite would rediscover as bugs — does
not transfer; at best its scenarios become a checklist.

Performance motivation is weak on the evidence: hot paths are
already native (whisper.cpp/ggml, ONNX Runtime, PortAudio,
PipeWire). Python overhead is in orchestration/UI, where Rust
would save memory/startup, not transcription latency.

8. xbps packaging (Void)

Primary sources: Void void-packages
https://github.com/void-linux/void-packages/blob/master/Manual.md ,
CONTRIBUTING.md in the same repo, and the cargo build style
itself:
https://raw.githubusercontent.com/void-linux/void-packages/master/common/build-style/cargo.sh

  • Rust packages use build_style=cargo; the style runs
    cargo auditable build --release --locked --target $RUST_TARGET
    and installs with --offline --locked. Implication: a committed
    Cargo.lock and vendored/offline-available crates are
    mandatory — the build cannot reach crates.io. Void handles this
    for existing Rust packages, but every dependency must be
    fetchable at template distfiles/vendor time.
  • Native -sys crates are the friction: whisper-rs-sys (cmake +
    C++ + optional Vulkan), pipewire-sys/libspa-sys (bindgen
    against Void's libpipewire headers — version skew vs the crate's
    expectations is a real failure mode), gtk4-sys (Void gtk4 +
    girepository versions), ort (needs ONNX Runtime binary or
    source build strategy). Each adds hostmakedepends/
    makedepends and cross-build (aarch64/musl — Void supports
    glibc and musl) exposure that the current Python package
    largely delegates to distro python3-gobject, gtk+3, wheels,
    and the existing installer.
  • Cross/musl note: cargo.sh cross path adds rust-std for the
    target; whisper.cpp/Vulkan under musl cross is the least-trodden
    part. The Python app already documents Void installs
    (DISTRO_COMPATIBILITY.md:196, distro package map §0) — a Rust
    xbps template starts from zero, including the runit service
    question for any ydotool-style helper and model-download policy
    (Void packages cannot download models at build time; runtime
    download with checksums, as today, must be preserved).
  • Acceptance: Void's CONTRIBUTING.md package requirements
    (maintainer commitment, build from source, no vendored prebuilt
    binaries) apply equally; nothing about Rust exempts the
    whisper.cpp/ggml vendoring question — it would need linking
    against a system whisper.cpp or a justified bundled build.

Net: xbps can package Rust well (many precedents), but this app's
native stack makes its template a project in itself, and musl +
Vulkan whisper.cpp is the riskiest cell. Python-on-Void already
works today.

9. Recommendation

  1. Stay on Python for the shipping app. Spend effort where
    users feel it: compositor injection coverage, Void/runit
    ydotool service docs, PipeWire-native capture.
  2. If Rust is strategically wanted, prototype the risk first,
    in this order, as throwaway spikes on real Void Wayland
    sessions (Sway/wlroots + KDE + GNOME):
    a. injection spike (ashpd RemoteDesktop + virtual-keyboard +
    IBus) proving Unicode dictation into GTK, Qt, Chromium, and
    a terminal;
    b. ksni tray + pipewire capture + whisper-rs dictation
    loop as a headless daemon driven by the existing Python UI
    (D-Bus boundary already exists: dbus_service.py);
    c. only then judge a gtk4-rs UI, knowing it is a GTK4 migration
    regardless of language.
  3. Revisit a full rewrite only if (a) and (b) succeed and a GTK4
    migration is independently decided. At that point, migrate
    Python→GTK4 first or accept a long dual-maintenance window.

Sources (primary)

# Rust rebuild viability — Vocalinux on Void Linux (Wayland) - Ticket: Gitea "Rust rebuild viability", map "Voculinux port" - Question: Is a Rust rebuild of the Vocalinux Python GTK3 tray dictation app sound on Void Linux Wayland, or stay on Python? - Method: local checkout (`src/vocalinux/`, v0.18.5) read directly; external claims traced to primary sources (official docs, protocol XML, crate docs/source, Void `void-packages` source). Secondary write-ups were used only as search leads, never as citations. - Date of local measurement: this checkout, commands quoted in §7. ## Verdict **Stay on Python. Do not do a wholesale Rust rebuild now.** A Rust rebuild is *technically feasible* area by area — PipeWire, whisper.cpp, tray, even GTK have real Rust bindings — but it is not *sound* as a rewrite, for three load-bearing reasons: 1. **The hard problems are not language problems.** Wayland text injection and global hotkeys are limited by compositor protocols and portal consent, identically in Rust and Python (§3). Rust buys nothing there, and the current app's value is precisely its multi-backend fallback ladder (IBus, RemoteDesktop portal, wtype, ydotool/uinput, xdotool/XWayland, clipboard paste). 2. **Rust forces a simultaneous GTK3 → GTK4 migration.** The Rust GTK3 bindings are unmaintained and dropped upstream (§5), so "port to Rust" secretly means "port language *and* toolkit *and* tray model at once" — three rewrites stacked, each able to break parity alone. 3. **Rebuild cost dwarfs plausible gain.** The app is ~48k LOC of Python with ~88k LOC of tests (§7). Inference already runs in native whisper.cpp via pywhispercpp; Python is orchestration around C/C++ engines, PortAudio, and D-Bus. Rust would mainly speed up code that is not the bottleneck, while discarding the test suite that encodes years of compositor/device edge cases. Sound Rust use, if desired: an *incremental* native sidecar or prototype (PipeWire capture + whisper.cpp core behind the existing Python UI, or a small tray-less dictation daemon) to prove injection on the actual Void compositors targeted — not a flag-day rewrite. See §9. ## 0. What exists today (local, primary) | Fact | Evidence | |---|---| | Python ≥3.11, GTK 3 via PyGObject (distro `python3-gi`, never pip) | `pyproject.toml` (`requires-python`, deps); `AGENTS.md` toolchain section | | Default engine whisper.cpp via `pywhispercpp>=1.5,<2`; optional OpenAI Whisper, Vosk, faster-whisper, Parakeet (sherpa-onnx), remote API | `pyproject.toml` `[project.optional-dependencies]`; `src/vocalinux/speech_recognition/recognition_manager.py` (5,691 lines) | | Mic capture via PyAudio/PortAudio; system-audio via PipeWire `pw-record`/`pw-dump` subprocesses | `src/vocalinux/audio/capture.py` (1,207 lines), `src/vocalinux/audio/pipewire.py` (572 lines) | | Silero VAD via `onnxruntime`, bundled `silero_vad.onnx` | `src/vocalinux/speech_recognition/silero_vad.py`, `data/` | | Injection backends: IBus engine, XDG RemoteDesktop portal, wtype, ydotool, xdotool, clipboard | `src/vocalinux/text_injection/text_injector.py` (3,489), `ibus_engine.py` (1,882), `remote_desktop_portal.py` (788) | | Tray via AyatanaAppIndicatorGlib 2.0 → AyatanaAppIndicator3 → AppIndicator3 fallback chain | `src/vocalinux/ui/tray_indicator.py:28-55` (1,983 lines), `main.py:272-288` | | Hotkeys: pynput (X11), evdev (Wayland) | `src/vocalinux/ui/keyboard_backends/{pynput_backend,evdev_backend}.py` | | GtkLayerShell overlay, libnotify, logind suspend, single-instance lock, D-Bus service | `ui/dictation_overlay.py`, `ui/notifications.py`, `suspend_handler.py`, `single_instance.py`, `dbus_service.py` | | Void already a supported install target | `scripts/distro-package-map.yaml:177-183` (`python3-gobject, gtk+3, libappindicator-gtk3, portaudio-devel, xdotool, wtype, ydotool…`), `docs/DISTRO_COMPATIBILITY.md:196`, `install.d/` xbps branches | | Size: 47,976 LOC Python (84 files), 87,649 LOC tests (155 files) | `wc -l` on `src/vocalinux/**/*.py`, `tests/test_*.py` (§7) | Note the Void-specific wrinkle already present in Python: the ydotool guidance in `text_injector.py:57-67` is systemd-centric, while Void uses runit — language choice does not fix service supervision either. ## 1. PipeWire capture — Rust: viable, arguably better - The official `pipewire` crate (0.10.1 docs) is "Rust bindings to `libpipewire`, the library for interacting with the pipewire server", hosted under freedesktop's PipeWire namespace, with `MainLoop`/`Context`/`Core`/`Registry` and stream APIs. Source: https://pipewire.pages.freedesktop.org/pipewire-rs/pipewire/ - PipeWire's own docs are the reference for the C API those bindings wrap: https://docs.pipewire.org/ - Alternative `cpal` (RustAudio) on Linux is ALSA-host based; PipeWire is reached through the ALSA `pipewire` bridge device, not a native graph API — fine for a mic, weaker for per-node/monitor capture. Source: https://docs.rs/cpal/latest/cpal/ and https://github.com/RustAudio/cpal Assessment: native `pipewire` crate > today's `pw-record` subprocess + `pw-dump` JSON parsing (`audio/pipewire.py`) and > PortAudio device negotiation pain documented at length in `audio/capture.py` (format/channel aborts, Bluetooth SCO heap corruption comments). But: the crate is pre-1.0, callback/`SPA` pod construction is verbose unsafe-adjacent work, and device-hotplug/format edge cases would need re-earning the hard way. **Net: genuine Rust win, medium effort, low risk.** This is the strongest pro-Rust area. ## 2. whisper.cpp inference — Rust: viable, parity likely - `whisper-rs` 0.16.0: "Rust bindings for whisper.cpp", safe wrapper over `whisper-rs-sys` (bindgen + CMake build of whisper.cpp/ggml); `WhisperContext::new_with_params` → `create_state` → `WhisperState::full`. Sources: https://docs.rs/whisper-rs/latest/whisper_rs/ , https://crates.io/crates/whisper-rs , repo https://codeberg.org/tazz4843/whisper-rs - Upstream engine: https://github.com/ggml-org/whisper.cpp - Silero VAD equivalent: `ort` (ONNX Runtime Rust bindings) https://docs.rs/ort/latest/ort/ — replaces Python `onnxruntime` used in `silero_vad.py`. Assessment: same underlying C++ engine as pywhispercpp, so transcription quality/speed should match; GPU backends (Vulkan etc.) become Cargo features/CMake flags instead of today's installer rebuild dance (`AGENTS.md`, `install.sh` PYWHISPERCPP_VERSION). Costs: long native builds, `-sys` build needs cmake + C++ toolchain at package-build time (matters for xbps, §8), and re-implementing the 5.7k-line `recognition_manager.py` logic (VAD segmentation, engine dispatch, model download/checksum pinning, keepalive/unload, diarization via tinydiarize in `diarization.py`, GPU probing workarounds for old pywhispercpp versions). Optional engines (Vosk/Parakeet/faster-whisper/remote) each need their own Rust story (sherpa-onnx has a C API; Vosk has a Rust binding; none is free). **Net: feasible, no quality gain, high porting effort in the orchestration layer around the engine.** ## 3. Wayland text injection — Rust: no advantage; the binding constraint This is the make-or-break area, and it is compositor-bound, not language-bound. There is still no stable, universal "type this string into the focused app" Wayland protocol: - `text-input-unstable-v3` is, by its own XML, *experimental*: "Warning! The protocol described in this file is experimental and backward incompatible changes may be made." It serves compositor↔client text fields via an input method; a dictation app cannot simply commit strings into arbitrary other clients. Source (primary XML): https://gitlab.freedesktop.org/wayland/wayland-protocols/-/raw/main/unstable/text-input/text-input-unstable-v3.xml - `input-method-unstable-v2` + `virtual-keyboard-unstable-v1` (the wlr-originated pair wtype uses) live outside stable wayland-protocols and are not implemented by every compositor — notably not by GNOME/Mutter — which is why the Python app probes wtype support and falls back (`text_injector.py:186-205`). Sources: https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/39 (input-method hub issue listing text-input-v3 / input-method / virtual-keyboard status), https://wayland.app/protocols/virtual-keyboard-unstable-v1 - The cross-desktop sanctioned path is the XDG **RemoteDesktop portal**: `NotifyKeyboardKeysym`/`NotifyKeyboardKeycode` on a consented session ("May only be called if KEYBOARD access was provided after starting the session"). Keysums, not Unicode strings — hence today's layout/keysym mapping work. Source: https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html Rust client: `ashpd` https://docs.rs/ashpd/latest/ashpd/desktop/remote_desktop/index.html - Remaining fallbacks are identical in any language: IBus engine (needs IBus as the active IM; KDE needs "IBus Wayland" virtual keyboard selected — `text_injector.py:100-106`), ydotool/uinput (needs `/dev/uinput` access, `input` group, a daemon — on Void a runit service, not systemd), clipboard + paste shortcut. Rust crates exist for the pieces (`wayland-client`, `wayland-protocols` crates: https://docs.rs/wayland-protocols/latest/wayland_protocols/ , `zbus`/`ashpd` for portals/IBus-over-D-Bus), but every compositor quirk, probe, and fallback currently encoded in ~6k lines of `text_injection/` + `focused_window.py` would have to be re-derived and re-tested per compositor (wlroots/Sway, KWin, Mutter at minimum). **Net: parity is achievable in principle, most expensive to re-prove, zero capability gain. Any Rust plan that starts with UI or inference and leaves injection for later has the risk order backwards.** Global hotkeys have the same shape: no general Wayland global-key grab; today's evdev backend (`/dev/input`, permissions) maps to Rust `evdev` crate (https://docs.rs/evdev/latest/evdev/) one-for-one, with the same privilege caveats. ## 4. StatusNotifier tray — Rust: viable, small - The tray protocol is D-Bus `org.kde.StatusNotifierItem` / `org.freedesktop.StatusNotifierWatcher` (+ `com.canonical.dbusmenu`); it was never a GTK API. Spec (draft, freedesktop): https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/ - Rust: `ksni` 0.3.x — "A Rust implementation of the KDE/freedesktop StatusNotifierItem specification", pure-Rust on zbus, implements the `Tray` trait. Sources: https://docs.rs/ksni/latest/ksni/ , https://github.com/iovxw/ksni - Today Python reaches the same protocol through libayatana- appindicator (`tray_indicator.py:37-53`, with a GMenuModel path in `tray_menu_model.py`). Assessment: `ksni` is a clean replacement and drops the Ayatana/AppIndicator typelib fallback chain entirely. Host support is unchanged: needs a StatusNotifierHost (KDE, Waybar/wlroots panels, XFCE; GNOME Shell needs the AppIndicator extension) — on a typical Void Wayland setup (Sway/Waybar, Wayfire, labwc) that is normally present. Icon-theme/pixmap, menu, and tooltip semantics need care but are bounded. **Net: genuine simplification, low effort.** Second-strongest pro-Rust area. ## 5. GTK UI parity — Rust: forces GTK4; largest hidden cost - Rust GTK3 bindings (`gtk` 0.18, gtk3-rs) state on their docs page: "This project is UNMAINTAINED. Please take a look at gtk4-rs instead!" https://docs.rs/gtk/0.18.2/gtk/ - gtk-rs officially dropped them: "Support for gtk3-rs crates was dropped. No more updates will be done on it. If you didn't yet, please migrate to gtk4-rs." (gtk4-rs 0.8 / gtk-rs-core 0.19 release post, 2024-06-01) https://gtk-rs.org/blog/2024/06/01/new-release.html - Supported path is `gtk4-rs`: https://github.com/gtk-rs/gtk4-rs , docs https://docs.rs/gtk4/latest/gtk4/ ; idiomatic layer `relm4` https://github.com/Relm4/Relm4 is optional. - GTK 3→4 itself removes/changes APIs the app relies on (migration guide: https://docs.gtk.org/gtk4/migrating-3to4.html) — e.g. `GtkStatusIcon` was removed in GTK4, containers/event handling changed; GtkLayerShell has a separate GTK4 variant (gtk4-layer-shell). Assessment: there is no "same UI, new language" option. The settings UI alone is `ui/settings_dialog.py` at 10,137 lines, plus tray, overlay, dictation pad, first-run, transcript, logging dialogs — ~25k lines under `ui/` total. Rewriting that in gtk4-rs means re-laying-out every dialog, re-doing GObject property/signal wiring from idiomatic Python into Rust's subclassing/builder style, and losing PyGObject's exact dynamic behaviours the tests pin down. GTK4 parity is *possible* (gtk4-rs is actively maintained), but it is a full UI rewrite, and GTK4-on-Wayland behavioural differences (popovers, layer-shell overlay, global shortcuts absence) land at the same time as the language change. **Net: feasible only as a GTK4 migration project; if that migration is ever wanted, doing it in Python/PyGObject first (GTK4 is available to Python today) is the lower-risk sequence.** ## 6. Comparison summary | Area | Rust today (primary source) | vs current Python | Effort / risk | |---|---|---|---| | PipeWire capture | `pipewire` crate 0.10, freedesktop-hosted [§1] | Better than pw-record subprocess; device graph native | Med / Low | | whisper.cpp | `whisper-rs` 0.16 over whisper.cpp [§2] | Same engine as pywhispercpp; no quality gain | High (orchestration) / Med | | VAD (Silero) | `ort` ONNX bindings [§2] | Parity | Low / Low | | Wayland injection | protocols unstable/compositor-gated; portal keysyms [§3] | **No gain**; fallback ladder must be rebuilt | High / **High** | | Global hotkey | `evdev` crate = today's evdev backend [§3] | Parity, same perms | Low / Med | | Tray (SNI) | `ksni` 0.3 on zbus [§4] | Simpler than Ayatana chain | Low / Low | | UI toolkit | gtk3-rs dead; gtk4-rs maintained [§5] | Forces GTK4 rewrite of ~25k LOC UI | **Very high / High** | | D-Bus/portals/notify/logind | `zbus`, `ashpd` | Parity with Gio/GLib usage | Med / Med | | xbps packaging | `build_style=cargo`, `--locked --offline` [§8] | Native -sys deps complicate; Python path already Void-tested | Med–High / Med | ## 7. Rebuild cost (measured, not imagined) Local measurement on this checkout: ```text src/vocalinux/**/*.py 47,976 lines, 84 files tests/test_*.py 87,649 lines, 155 files recognition_manager.py 5,691 settings_dialog.py 10,137 text_injector.py 3,489 tray_indicator.py 1,983 ibus_engine.py 1,882 capture.py 1,207 ``` Inference (estimate, labelled as such): a like-for-like Rust port is not a transliteration — GObject/GTK, asyncio-free threading, and subprocess backends all change shape. At typical rewrite rates for systems/desktop code with hardware-in-the-loop testing, parity is a multi-person-month project (order 6–12+ months for one developer to reach *tested* parity across compositors and audio devices), during which the Python app either freezes or the port chases a moving target (v0.18.5 and releasing regularly per `docs/RELEASE_PROCESS.md`). The 88k-line test suite — which pins exactly the edge cases a rewrite would rediscover as bugs — does not transfer; at best its *scenarios* become a checklist. Performance motivation is weak on the evidence: hot paths are already native (whisper.cpp/ggml, ONNX Runtime, PortAudio, PipeWire). Python overhead is in orchestration/UI, where Rust would save memory/startup, not transcription latency. ## 8. xbps packaging (Void) Primary sources: Void `void-packages` https://github.com/void-linux/void-packages/blob/master/Manual.md , `CONTRIBUTING.md` in the same repo, and the cargo build style itself: https://raw.githubusercontent.com/void-linux/void-packages/master/common/build-style/cargo.sh - Rust packages use `build_style=cargo`; the style runs `cargo auditable build --release --locked --target $RUST_TARGET` and installs with `--offline --locked`. Implication: a committed `Cargo.lock` and **vendored/offline-available crates** are mandatory — the build cannot reach crates.io. Void handles this for existing Rust packages, but every dependency must be fetchable at template `distfiles`/vendor time. - Native `-sys` crates are the friction: `whisper-rs-sys` (cmake + C++ + optional Vulkan), `pipewire-sys`/`libspa-sys` (bindgen against Void's libpipewire headers — version skew vs the crate's expectations is a real failure mode), `gtk4-sys` (Void gtk4 + girepository versions), `ort` (needs ONNX Runtime binary or source build strategy). Each adds `hostmakedepends`/ `makedepends` and cross-build (aarch64/musl — Void supports glibc **and musl**) exposure that the current Python package largely delegates to distro `python3-gobject`, `gtk+3`, wheels, and the existing installer. - Cross/musl note: `cargo.sh` cross path adds `rust-std` for the target; whisper.cpp/Vulkan under musl cross is the least-trodden part. The Python app already documents Void installs (`DISTRO_COMPATIBILITY.md:196`, distro package map §0) — a Rust xbps template starts from zero, including the runit service question for any ydotool-style helper and model-download policy (Void packages cannot download models at build time; runtime download with checksums, as today, must be preserved). - Acceptance: Void's `CONTRIBUTING.md` package requirements (maintainer commitment, build from source, no vendored prebuilt binaries) apply equally; nothing about Rust exempts the whisper.cpp/ggml vendoring question — it would need linking against a system whisper.cpp or a justified bundled build. **Net: xbps can package Rust well (many precedents), but this app's native stack makes its template a project in itself, and musl + Vulkan whisper.cpp is the riskiest cell. Python-on-Void already works today.** ## 9. Recommendation 1. **Stay on Python** for the shipping app. Spend effort where users feel it: compositor injection coverage, Void/runit ydotool service docs, PipeWire-native capture. 2. If Rust is strategically wanted, **prototype the risk first**, in this order, as throwaway spikes on real Void Wayland sessions (Sway/wlroots + KDE + GNOME): a. injection spike (`ashpd` RemoteDesktop + virtual-keyboard + IBus) proving Unicode dictation into GTK, Qt, Chromium, and a terminal; b. `ksni` tray + `pipewire` capture + `whisper-rs` dictation loop as a headless daemon driven by the existing Python UI (D-Bus boundary already exists: `dbus_service.py`); c. only then judge a gtk4-rs UI, knowing it is a GTK4 migration regardless of language. 3. Revisit a full rewrite only if (a) and (b) succeed *and* a GTK4 migration is independently decided. At that point, migrate Python→GTK4 first or accept a long dual-maintenance window. ## Sources (primary) - Local: `pyproject.toml`, `AGENTS.md`, `src/vocalinux/{audio,speech_recognition,text_injection,ui}/`, `scripts/distro-package-map.yaml`, `docs/DISTRO_COMPATIBILITY.md` - PipeWire Rust: https://pipewire.pages.freedesktop.org/pipewire-rs/pipewire/ ; https://docs.pipewire.org/ - cpal: https://docs.rs/cpal/latest/cpal/ ; https://github.com/RustAudio/cpal - whisper-rs: https://docs.rs/whisper-rs/latest/whisper_rs/ ; https://crates.io/crates/whisper-rs ; https://codeberg.org/tazz4843/whisper-rs ; whisper.cpp: https://github.com/ggml-org/whisper.cpp - ort: https://docs.rs/ort/latest/ort/ - Wayland protocols: https://gitlab.freedesktop.org/wayland/wayland-protocols/-/raw/main/unstable/text-input/text-input-unstable-v3.xml ; https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/39 ; https://wayland.app/protocols/virtual-keyboard-unstable-v1 ; https://docs.rs/wayland-protocols/latest/wayland_protocols/ - XDG portal RemoteDesktop: https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.RemoteDesktop.html ; ashpd: https://docs.rs/ashpd/latest/ashpd/ - StatusNotifierItem: https://www.freedesktop.org/wiki/Specifications/StatusNotifierItem/ ; ksni: https://docs.rs/ksni/latest/ksni/ , https://github.com/iovxw/ksni - gtk-rs: https://gtk-rs.org/blog/2024/06/01/new-release.html ; https://docs.rs/gtk/0.18.2/gtk/ ; https://github.com/gtk-rs/gtk4-rs ; GTK migration: https://docs.gtk.org/gtk4/migrating-3to4.html - evdev crate: https://docs.rs/evdev/latest/evdev/ - Void: https://github.com/void-linux/void-packages/blob/master/Manual.md ; https://raw.githubusercontent.com/void-linux/void-packages/master/common/build-style/cargo.sh ; https://raw.githubusercontent.com/void-linux/void-packages/master/common/environment/build-style/cargo.sh
Sign in to join this conversation.