0cea6a4b28
- 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>
157 lines
4.7 KiB
Rust
157 lines
4.7 KiB
Rust
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()?;
|
|
let yaml = serde_yaml::to_string(&config)?;
|
|
println!("{yaml}");
|
|
Ok(())
|
|
}
|
|
|
|
pub fn reset() -> Result<()> {
|
|
let config = Config::default();
|
|
config.save()?;
|
|
println!("Config reset to defaults at {}", Config::path()?.display());
|
|
Ok(())
|
|
}
|
|
|
|
pub fn interactive() -> Result<()> {
|
|
let mut config = Config::load()?;
|
|
|
|
config.hotkey = prompt_hotkey("Hotkey", &config.hotkey)?;
|
|
|
|
let mode_idx = Select::new()
|
|
.with_prompt("Recording mode")
|
|
.items(&["push_to_talk", "toggle"])
|
|
.default(match config.mode {
|
|
RecordingMode::PushToTalk => 0,
|
|
RecordingMode::Toggle => 1,
|
|
})
|
|
.interact()?;
|
|
config.mode = match mode_idx {
|
|
0 => RecordingMode::PushToTalk,
|
|
_ => RecordingMode::Toggle,
|
|
};
|
|
|
|
config.cancel_key = prompt_hotkey("Cancel key", &config.cancel_key)?;
|
|
|
|
config.model = Input::new()
|
|
.with_prompt("Model")
|
|
.default(config.model)
|
|
.interact_text()?;
|
|
|
|
let accel_idx = Select::new()
|
|
.with_prompt("Accelerator")
|
|
.items(&["auto", "cpu", "cuda", "directml"])
|
|
.default(match config.accelerator {
|
|
Accelerator::Auto => 0,
|
|
Accelerator::Cpu => 1,
|
|
Accelerator::Cuda => 2,
|
|
Accelerator::DirectMl => 3,
|
|
})
|
|
.interact()?;
|
|
config.accelerator = match accel_idx {
|
|
0 => Accelerator::Auto,
|
|
1 => Accelerator::Cpu,
|
|
2 => Accelerator::Cuda,
|
|
_ => Accelerator::DirectMl,
|
|
};
|
|
|
|
config.gpu_device = Input::new()
|
|
.with_prompt("GPU device index")
|
|
.default(config.gpu_device)
|
|
.interact_text()?;
|
|
|
|
let paste_idx = Select::new()
|
|
.with_prompt("Paste method")
|
|
.items(&["ctrl_v", "shift_insert", "ctrl_shift_v", "clipboard_only"])
|
|
.default(match config.paste_method {
|
|
PasteMethod::CtrlV => 0,
|
|
PasteMethod::ShiftInsert => 1,
|
|
PasteMethod::CtrlShiftV => 2,
|
|
PasteMethod::ClipboardOnly => 3,
|
|
})
|
|
.interact()?;
|
|
config.paste_method = match paste_idx {
|
|
0 => PasteMethod::CtrlV,
|
|
1 => PasteMethod::ShiftInsert,
|
|
2 => PasteMethod::CtrlShiftV,
|
|
_ => PasteMethod::ClipboardOnly,
|
|
};
|
|
|
|
let overlay_idx = Select::new()
|
|
.with_prompt("Overlay position")
|
|
.items(&["top", "bottom", "none"])
|
|
.default(match config.overlay_position {
|
|
OverlayPosition::Top => 0,
|
|
OverlayPosition::Bottom => 1,
|
|
OverlayPosition::None => 2,
|
|
})
|
|
.interact()?;
|
|
config.overlay_position = match overlay_idx {
|
|
0 => OverlayPosition::Top,
|
|
1 => OverlayPosition::Bottom,
|
|
_ => OverlayPosition::None,
|
|
};
|
|
|
|
let feedback_idx = Select::new()
|
|
.with_prompt("Audio feedback")
|
|
.items(&["yes", "no"])
|
|
.default(if config.audio_feedback { 0 } else { 1 })
|
|
.interact()?;
|
|
config.audio_feedback = feedback_idx == 0;
|
|
|
|
let vad_idx = Select::new()
|
|
.with_prompt("VAD (voice activity detection)")
|
|
.items(&["enabled", "disabled"])
|
|
.default(if config.vad_enabled { 0 } else { 1 })
|
|
.interact()?;
|
|
config.vad_enabled = vad_idx == 0;
|
|
|
|
config.language = Input::new()
|
|
.with_prompt("Language")
|
|
.default(config.language)
|
|
.interact_text()?;
|
|
|
|
config.save()?;
|
|
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()),
|
|
}
|
|
}
|