v0.2.0: System tray, IPC status, VAD, hotkey grab, and polish

- Add system tray icon with Exit menu (tray-icon/muda)
- Add IPC daemon status via named pipe (Windows) / Unix socket (Linux)
- Add `mouth status` command to query running daemon
- Add daemon lock to prevent multiple instances
- Hide Windows console window when running as daemon
- Wire up Silero VAD model download and speech filtering
- Switch hotkey listener from rdev::listen to rdev::grab to consume hotkeys
- Add hotkey capture mode in interactive config (press keys instead of typing)
- Add all missing key names (brackets, punctuation, numpad, etc.)
- Fix ONNX tensor type mismatches (encoder wants i64, decoder wants i32)
- Add 300ms lead-in silence to compensate for mic startup latency
- Add 300ms trailing recording after stop for speech not to be clipped
- Add 50ms silence before audio feedback blips for device warmup
- Reduce overlay size (150x18, was 200x36)
- Add PolyForm Noncommercial 1.0.0 license
- Flesh out user-focused README
- Update release script with Gitea/GitHub forge support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 22:04:39 +01:00
parent f9d65ff850
commit 0cea6a4b28
19 changed files with 1948 additions and 490 deletions
+37 -8
View File
@@ -1,7 +1,9 @@
use anyhow::Result;
use dialoguer::{Input, Select};
use std::time::Duration;
use crate::config::{Accelerator, Config, OverlayPosition, PasteMethod, RecordingMode};
use crate::hotkey::capture_hotkey;
pub fn show() -> Result<()> {
let config = Config::load()?;
@@ -20,10 +22,7 @@ pub fn reset() -> Result<()> {
pub fn interactive() -> Result<()> {
let mut config = Config::load()?;
config.hotkey = Input::new()
.with_prompt("Hotkey")
.default(config.hotkey)
.interact_text()?;
config.hotkey = prompt_hotkey("Hotkey", &config.hotkey)?;
let mode_idx = Select::new()
.with_prompt("Recording mode")
@@ -38,10 +37,7 @@ pub fn interactive() -> Result<()> {
_ => RecordingMode::Toggle,
};
config.cancel_key = Input::new()
.with_prompt("Cancel key")
.default(config.cancel_key)
.interact_text()?;
config.cancel_key = prompt_hotkey("Cancel key", &config.cancel_key)?;
config.model = Input::new()
.with_prompt("Model")
@@ -125,3 +121,36 @@ pub fn interactive() -> Result<()> {
println!("\nConfig saved to {}", Config::path()?.display());
Ok(())
}
/// Prompt the user to either press a key combination or type it manually.
fn prompt_hotkey(label: &str, current: &str) -> Result<String> {
let choice = Select::new()
.with_prompt(format!("{label} (current: {current})"))
.items(&["Press the key combination", "Type it manually", "Keep current"])
.default(0)
.interact()?;
match choice {
0 => {
println!("Press your desired key combination (timeout: 10s)...");
match capture_hotkey(Duration::from_secs(10)) {
Some(hotkey) => {
println!(" Captured: {hotkey}");
Ok(hotkey)
}
None => {
println!(" No keypress detected, keeping current: {current}");
Ok(current.to_string())
}
}
}
1 => {
let value = Input::new()
.with_prompt(label)
.default(current.to_string())
.interact_text()?;
Ok(value)
}
_ => Ok(current.to_string()),
}
}