Files
mouth/src/cli/config_cmd.rs
T
steve 0cea6a4b28 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>
2026-04-10 22:04:39 +01:00

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()),
}
}