-
Notifications
You must be signed in to change notification settings - Fork 0
development
-
Rust 1.75+ with
cargo— rustup.rs -
Node.js 18+ with
npm -
Tauri CLI 2.x —
cargo install tauri-cli
# Ubuntu / Debian
sudo apt install \
libwebkit2gtk-4.1-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libssl-dev \
libasound2-dev \
libspeechd-dev \
pkg-config \
build-essential
# Fedora
sudo dnf install \
webkit2gtk4.1-devel \
libayatana-appindicator-devel \
openssl-devel \
alsa-lib-devel \
speech-dispatcher-devel- Visual Studio Build Tools 2019+ (select the "Desktop development with C++" workload)
- WebView2 Runtime (pre-installed on Windows 10 21H2+ and Windows 11)
- Rust MSVC toolchain:
rustup default stable-x86_64-pc-windows-msvc
For full Windows build instructions and a PowerShell helper script, see docs/windows_build.md.
VoxCtrl/
├── src/ # Svelte frontend
│ ├── main.ts
│ ├── App.svelte
│ ├── stores/
│ │ ├── config.ts
│ │ └── status.ts
│ └── lib/
│ ├── Settings/
│ ├── Overlay/
│ └── History/
│
├── src-tauri/ # Tauri application shell
│ ├── Cargo.toml
│ ├── tauri.conf.json
│ └── src/
│ ├── main.rs
│ ├── lib.rs # Main coordinator
│ ├── commands.rs # IPC command handlers
│ └── state.rs # AppState definition
│
├── crates/ # Backend library crates
│ ├── voxctrl-config/
│ ├── voxctrl-audio/
│ ├── voxctrl-hotkeys/
│ ├── voxctrl-inference/
│ ├── voxctrl-routing/
│ ├── voxctrl-inject/
│ ├── voxctrl-tts/
│ ├── voxctrl-mcp/
│ ├── voxctrl-dbus/
│ └── voxctrl-llm/
│
├── Cargo.toml # Workspace definition
├── package.json # Frontend deps
├── vite.config.ts
└── svelte.config.js
npm install # Install frontend deps (first time only)
npm run tauri dev # Start Tauri + Vite in development modeThis:
- Starts Vite dev server on
http://localhost:5173with HMR - Compiles the Rust backend
- Launches the app with the WebView pointed at Vite
Svelte changes hot-reload instantly. Rust changes trigger a backend recompile (typically 5–30s).
If you only need to work on the UI:
npm run dev
# Opens http://localhost:5173 in browser
# Note: Tauri commands won't work in browser — mock them if neededcargo build -p voxctrl-inference # Build a specific crate
cargo test -p voxctrl-config # Test a specific crate
cargo check --workspace # Type-check all cratesbash build_appimage.sh
# Output: VoxCtrl.AppImage in project rootThe build script:
- Runs
npm run tauri buildto produce a.debbundle - Extracts the contents into an AppDir
- Runs
appimagetoolto create the AppImage
npm run tauri build
# Output: src-tauri/target/release/bundle/
# Linux: .deb, .AppImage
# Windows: .msi, .exe (NSIS)CUDA inference acceleration is disabled by default so the app builds on any machine. Enable it with the cuda cargo feature:
# Linux / macOS
npm run tauri build -- --features cuda
# Windows (PowerShell)
npm run tauri build -- --features cuda
# Or use the helper script:
.\scripts\build_windows.ps1 -CudaThe cuda feature propagates: voxctrl-app/cuda → voxctrl-inference/cuda → whisper-rs/cuda.
Each crate under crates/ is self-contained. They are included in the workspace Cargo.toml and referenced by src-tauri as path dependencies.
cargo new --lib crates/voxctrl-myfeature
# Add to Cargo.toml workspace members:
[workspace]
members = [
...
"crates/voxctrl-myfeature",
]
# Reference from src-tauri/Cargo.toml:
voxctrl-myfeature = { path = "../crates/voxctrl-myfeature" }- Keep each crate focused on one domain
- Expose a minimal public API (
pubon types/functions needed by callers) - Use
tokiofor async where I/O is needed; keep CPU-heavy work on dedicated OS threads - Pass channels rather than
Arc<Mutex<_>>for data pipelines where possible
The main coordinator. This is where the audio pipeline is assembled:
- Creates all channels
- Spawns the hotkey listener
- Spawns the audio recorder
- Spawns the inference worker
- Starts the MCP server
- Starts the DBus service
- Runs the Tauri event loop with the status ticker
When adding a new integration, this is typically where you wire it in.
All #[tauri::command] handlers. Each command is a thin wrapper that reads/writes AppState or calls into a crate. Keep commands small — business logic belongs in crates.
The AppConfig struct is the source of truth for all settings. If you add a config option, add it here first, then expose it in the Settings UI.
Defines OutputTarget, HotkeyBinding, DeliveryType, TargetProcessingConfig, and GestureType. Add new delivery types or target fields here.
- Add a variant to
DeliveryTypeenum incrates/voxctrl-routing/src/models.rs - Add any target-specific fields to
OutputTargetincrates/voxctrl-routing/src/models.rs - Add a match arm in the router dispatch logic in
crates/voxctrl-routing/src/router.rs - Update the TypeScript
OutputTargetinterface insrc/stores/config.ts - Add the new type to the delivery type selector in
src/lib/Settings/RoutingTab.svelte - Document in
docs/routing.md
VoxCtrl utilizes a multi-tiered, unified testing suite spanning Svelte frontend components, Rust backend crates, and end-to-end integration tests over local socket connections.
The easiest way to run the entire test suite (Rust, Svelte, and Pytest Integration) is via the master test runner script:
npm testThis runs python3 scripts/run_tests.py, which sequences the following three test suites and returns a consolidated exit code (cleanly skipping the integration tests with a warning if pytest is not installed on the system):
-
Rust Backend tests (
cargo test) -
Svelte Frontend tests (
npm run test:unit) -
Python Integration tests (
pytest tests/integration/)
Backend logic, including settings schemas, migrations, routing models, and utilities, is tested using standard Rust/Cargo unit tests.
# Run all tests across the entire workspace
cargo test --workspace
# Run tests for a specific backend crate
cargo test -p voxctrl-config
cargo test -p voxctrl-routingBackend unit tests are written inside their respective crate files within a #[cfg(test)] module block.
Example from crates/voxctrl-config/src/lib.rs:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config_values() {
let cfg = AppConfig::default();
assert!(cfg.ui.auto_show_settings);
assert_eq!(cfg.ui.overlay_style, OverlayStyle::BlueWave);
}
}Frontend Svelte 5 components, settings views, and warning overlays are tested using Vitest, JSDOM, and Svelte Testing Library.
-
Test Location:
tests/svelte/(files ending in.test.ts) -
Framework Stack: Vitest (runner), jsdom (DOM environment),
@testing-library/svelte(rendering & selectors)
# Run all frontend tests once
npm run test:unit
# Run frontend tests in interactive watch mode
npx vitestTauri commands (invoke) and events (listen) are mocked inside Svelte tests using Vitest's vi.mock to ensure they run successfully in headless/JSDOM environments without a live Webview context.
Example from tests/svelte/EngineTab.test.ts:
import { describe, test, expect, vi } from "vitest";
import { render, screen } from "@testing-library/svelte";
import EngineTab from "../../src/lib/Settings/EngineTab.svelte";
// Mock Tauri core commands
vi.mock("@tauri-apps/api/core", () => ({
invoke: vi.fn(async (cmd, args) => {
if (cmd === "check_model_downloaded") {
return args.modelSize === "base"; // mock "base" downloaded, others missing
}
return true;
}),
}));
// Mock Tauri events
vi.mock("@tauri-apps/api/event", () => ({
listen: vi.fn(async () => {
return () => {}; // return clean unsubscribe function
}),
}));When testing Svelte components:
- Render the component using
render(Component, { props }). - Locate elements using Svelte Testing Library selectors (e.g.,
screen.findByTextorscreen.queryByText). - Assert behaviors using Vitest's
expect().
Example:
describe("EngineTab.svelte Warning Banner", () => {
test("shows warning banner if Whisper voice model is not downloaded", async () => {
const mockConfig = {
engine: {
backend: "whisper-cpp",
whisper_cpp: { model_size: "large-v3" },
}
} as any;
render(EngineTab, { cfg: mockConfig });
// Assert warning banner is found
const title = await screen.findByText("Voice Model Not Downloaded");
expect(title).not.toBeNull();
});
});Integration tests verify end-to-end communication channels such as the Model Context Protocol (MCP) server over Unix domain sockets (/tmp/voxctrl-mcp.sock).
-
Test Location:
tests/integration/(files prefixed withtest_) - Framework: Pytest
# Ensure pytest is installed
pip install pytest
# Run integration tests
pytest tests/integration/Note: These tests check for the live socket connection. If VoxCtrl is not currently running, these tests will gracefully skip to prevent false failure reports.
Integration tests use the standard pytest framework, creating client socket connections to communicate with /tmp/voxctrl-mcp.sock over JSON-RPC.
Example:
import socket
import json
import pytest
import os
SOCKET_PATH = "/tmp/voxctrl-mcp.sock"
@pytest.mark.skipif(not os.path.exists(SOCKET_PATH), reason="MCP Socket not running")
def test_mcp_handshake_and_tools():
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(SOCKET_PATH)
try:
# Send a standard JSON-RPC request to the MCP server
payload = {"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}
sock.sendall((json.dumps(payload) + "\n").encode('utf-8'))
# Read and parse response
resp = json.loads(sock.recv(1024).decode('utf-8').strip())
assert "result" in resp
assert "tools" in resp["result"]
finally:
sock.close()Linux global hotkeys require specific udev permissions and user group memberships configured by install.sh. To make testing these startup states safe and easy, developers can use the VOXCTRL_TEST_UDEV_STATUS environment variable to mock various diagnostic outcomes without mutating their own user accounts or system rules.
The application employs robust permission checks at startup and via the check_udev_status Tauri IPC command:
-
Rule File Compatibility: The app checks for the existence of
/etc/udev/rules.d/99-voxctrl.rules,/etc/udev/rules.d/99-voxctl.rules(legacy name), or/etc/udev/rules.d/99-voxctr.rules(legacy name). If any match, rules are recognized as configured. -
Active vs NSS Group Database Verification: It checks if the active process session belongs to the
inputgroup. If missing, it queries the NSS system group database (id -Gn <username>) as a fallback. This handles persistent containerized development environments (where process group tokens do not refresh) gracefully, preventing false warning windows once the installer has been run. -
Windows Exclusions: Non-Linux environments (such as Windows builds) completely compile out udev diagnostic checks on startup and return fully bypassed success payloads (
is_configured: true), ensuring the warning screen never displays on Windows.
- Onboarding Verification: Ensure that new users are clearly prompted to install required dependencies.
-
Troubleshooting Relogins: Verify the specific advice prompting the user to reboot or log out if they ran
install.shbut didn't refresh their session. - Layout Integrity: Make sure the modal overlays perfectly on the dark obsidian theme on launch.
-
Simulate Missing Setup (Installer never run): Simulates
/etc/udev/rules.d/99-voxctrl.rulesdoes not exist:VOXCTRL_TEST_UDEV_STATUS=missing npm run tauri dev
-
UI Outcome: Spawns a standalone native window (
udev-warning) in the foreground detailing the need for hardware udev rules, providing a direct 🔧 Setup System Integration button to run setup automatically, and a Continue Anyway native window close pathway.
-
UI Outcome: Spawns a standalone native window (
-
Simulate Relogin Required (Installer run but session not updated): Simulates that rules exist but the current shell process is missing
inputgroup permissions:VOXCTRL_TEST_UDEV_STATUS=relogin npm run tauri dev
-
UI Outcome: Spawns a standalone native window (
udev-warning) displaying the explicit logout/relogin guidance (hiding the installer download CTA since the rules are already present).
-
UI Outcome: Spawns a standalone native window (
-
Simulate Normal/Configured State (Bypasses checks):
VOXCTRL_TEST_UDEV_STATUS=ok npm run tauri dev
- UI Outcome: Spawns only the standard settings window; the diagnostic warning window remains completely hidden.
VoxCtrl uses the log crate with env_logger. Enable verbose output:
RUST_LOG=debug npm run tauri dev
RUST_LOG=voxctrl_inference=trace npm run tauri devIn dev mode, right-click the Tauri window → Inspect Element to open WebKit DevTools.
Add console.log around invoke() calls in Svelte, or add println! in command handlers in Rust.
# Check CPAL devices
RUST_LOG=cpal=debug npm run tauri dev
# Check PulseAudio
pactl list sources short