use crate::daemon::{self, ToggleOutcome};
use crate::dictate::DictateOpts;
use crate::error::TalkError;
use crate::paste::get_active_window;
fn build_daemon_args(opts: &DictateOpts, target_window: Option<String>) -> Vec<String> {
let mut args = Vec::new();
if opts.verbose > 0 {
args.push(format!("-{}", "v".repeat(opts.verbose as usize)));
}
args.push("dictate".to_string());
args.push("--daemon".to_string());
if let Some(p) = opts.provider {
args.push("--provider".to_string());
args.push(p.to_string());
}
if let Some(ref m) = opts.model {
args.push("--model".to_string());
args.push(m.clone());
}
if opts.diarize {
args.push("--diarize".to_string());
}
if opts.timestamp {
args.push("--timestamp".to_string());
}
if opts.realtime {
args.push("--realtime".to_string());
}
if opts.no_sounds {
args.push("--no-sounds".to_string());
}
if opts.no_boop {
args.push("--no-boop".to_string());
}
if opts.no_chunk_paste {
args.push("--no-chunk-paste".to_string());
}
if opts.no_paste {
args.push("--no-paste".to_string());
}
if opts.monitor {
args.push("--monitor".to_string());
}
if opts.no_overlay {
args.push("--no-overlay".to_string());
}
if opts.no_auto_pause {
args.push("--no-auto-pause".to_string());
}
if let Some(mode) = opts.viz {
args.push("--viz".to_string());
args.push(mode.to_string());
}
if opts.mono {
args.push("--mono".to_string());
}
if opts.upload_format != crate::transcription::UploadFormat::Wav {
args.push("--upload-format".to_string());
args.push(format!("{:?}", opts.upload_format).to_lowercase());
}
if opts.no_bt_auto_switch {
args.push("--no-bt-auto-switch".to_string());
}
if let Some(ref path) = opts.save {
args.push("--save".to_string());
args.push(path.to_string_lossy().to_string());
}
if let Some(ref path) = opts.output_yaml {
args.push("--output-yaml".to_string());
args.push(path.to_string_lossy().to_string());
}
if let Some(ref path) = opts.input_audio_file {
args.push("--input-audio-file".to_string());
args.push(path.to_string_lossy().to_string());
}
if opts.retry_last {
args.push("--retry-last".to_string());
}
if opts.pick {
args.push("--pick".to_string());
}
if opts.replace_last_paste {
args.push("--replace-last-paste".to_string());
}
if let Some(ref wid) = target_window {
args.push("--target-window".to_string());
args.push(wid.clone());
}
args
}
pub async fn toggle_dispatch(opts: &DictateOpts) -> Result<(), TalkError> {
let slot = daemon::dictate_slot()?;
let outcome = daemon::toggle_current_executable(&slot, || async {
let target_window = get_active_window().await;
Ok(build_daemon_args(opts, target_window))
})
.await?;
match outcome {
ToggleOutcome::Started { pid, log_path } => log::info!(
"dictation started (PID {}, logs: {})",
pid,
log_path.display()
),
ToggleOutcome::Signalled { pid } => slot.trace(&format!(
"[DBG] dictate toggle sent SIGINT to daemon PID {pid}"
)),
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn test_opts() -> DictateOpts {
DictateOpts {
save: None,
output_yaml: None,
input_audio_file: None,
retry_last: false,
pick: false,
replace_last_paste: false,
provider: None,
model: None,
diarize: false,
timestamp: false,
realtime: false,
toggle: false,
no_sounds: false,
no_boop: false,
no_chunk_paste: false,
no_paste: false,
monitor: false,
no_overlay: false,
no_auto_pause: false,
viz: None,
mono: false,
upload_format: crate::transcription::UploadFormat::Wav,
no_bt_auto_switch: false,
daemon: false,
target_window: None,
verbose: 0,
}
}
#[test]
fn test_build_daemon_args_includes_timestamp() {
let mut opts = test_opts();
opts.timestamp = true;
let args = build_daemon_args(&opts, None);
assert!(
args.contains(&"--timestamp".to_string()),
"--timestamp should be forwarded to daemon"
);
}
#[test]
fn test_build_daemon_args_includes_no_paste() {
let mut opts = test_opts();
opts.no_paste = true;
let args = build_daemon_args(&opts, None);
assert!(
args.contains(&"--no-paste".to_string()),
"--no-paste should be forwarded to daemon"
);
}
#[test]
fn test_build_daemon_args_includes_pick() {
let mut opts = test_opts();
opts.pick = true;
let args = build_daemon_args(&opts, None);
assert!(
args.contains(&"--pick".to_string()),
"--pick should be forwarded to daemon"
);
}
#[test]
fn test_build_daemon_args_includes_retry_last() {
let mut opts = test_opts();
opts.retry_last = true;
let args = build_daemon_args(&opts, None);
assert!(
args.contains(&"--retry-last".to_string()),
"--retry-last should be forwarded to daemon"
);
}
#[test]
fn test_build_daemon_args_includes_replace_last_paste() {
let mut opts = test_opts();
opts.replace_last_paste = true;
let args = build_daemon_args(&opts, None);
assert!(
args.contains(&"--replace-last-paste".to_string()),
"--replace-last-paste should be forwarded to daemon"
);
}
#[test]
fn test_build_daemon_args_includes_input_audio_file() {
let mut opts = test_opts();
opts.input_audio_file = Some(PathBuf::from("/tmp/test.ogg"));
let args = build_daemon_args(&opts, None);
let idx = args
.iter()
.position(|a| a == "--input-audio-file")
.expect("--input-audio-file should be present");
assert_eq!(args[idx + 1], "/tmp/test.ogg");
}
#[test]
fn test_build_daemon_args_includes_output_yaml() {
let mut opts = test_opts();
opts.output_yaml = Some(PathBuf::from("/tmp/out.yaml"));
let args = build_daemon_args(&opts, None);
let idx = args
.iter()
.position(|a| a == "--output-yaml")
.expect("--output-yaml should be present");
assert_eq!(args[idx + 1], "/tmp/out.yaml");
}
#[test]
fn test_build_daemon_args_does_not_include_toggle() {
let mut opts = test_opts();
opts.toggle = true;
opts.daemon = true;
let args = build_daemon_args(&opts, None);
assert!(
!args.contains(&"--toggle".to_string()),
"--toggle should NOT be forwarded (already handled)"
);
let daemon_count = args.iter().filter(|a| *a == "--daemon").count();
assert_eq!(daemon_count, 1, "--daemon should appear exactly once");
}
#[test]
fn test_build_daemon_args_includes_target_window() {
let opts = test_opts();
let args = build_daemon_args(&opts, Some("0x12345678".to_string()));
let idx = args
.iter()
.position(|a| a == "--target-window")
.expect("--target-window should be present");
assert_eq!(args[idx + 1], "0x12345678");
}
#[test]
fn test_build_daemon_args_omits_unset_flags() {
let opts = test_opts();
let args = build_daemon_args(&opts, None);
assert!(!args.contains(&"--timestamp".to_string()));
assert!(!args.contains(&"--no-paste".to_string()));
assert!(!args.contains(&"--diarize".to_string()));
assert!(!args.contains(&"--realtime".to_string()));
assert!(!args.contains(&"--no-sounds".to_string()));
}
#[test]
fn test_build_daemon_args_exact_vector() {
let mut opts = test_opts();
opts.verbose = 2;
opts.provider = Some(crate::config::Provider::OpenAI);
opts.model = Some("gpt-test".to_string());
opts.diarize = true;
opts.timestamp = true;
opts.realtime = true;
opts.no_sounds = true;
opts.no_boop = true;
opts.no_chunk_paste = true;
opts.no_paste = true;
opts.monitor = true;
opts.no_overlay = true;
opts.no_auto_pause = true;
opts.viz = Some(crate::config::VizMode::Waterfall);
opts.mono = true;
opts.upload_format = crate::transcription::UploadFormat::Ogg;
opts.no_bt_auto_switch = true;
opts.save = Some(PathBuf::from("/tmp/save.ogg"));
opts.output_yaml = Some(PathBuf::from("/tmp/output.yaml"));
opts.input_audio_file = Some(PathBuf::from("/tmp/input.ogg"));
opts.retry_last = true;
opts.pick = true;
opts.replace_last_paste = true;
opts.toggle = true;
opts.daemon = true;
let args = build_daemon_args(&opts, Some("0x1234".to_string()));
assert_eq!(
args,
vec![
"-vv",
"dictate",
"--daemon",
"--provider",
"openai",
"--model",
"gpt-test",
"--diarize",
"--timestamp",
"--realtime",
"--no-sounds",
"--no-boop",
"--no-chunk-paste",
"--no-paste",
"--monitor",
"--no-overlay",
"--no-auto-pause",
"--viz",
"waterfall",
"--mono",
"--upload-format",
"ogg",
"--no-bt-auto-switch",
"--save",
"/tmp/save.ogg",
"--output-yaml",
"/tmp/output.yaml",
"--input-audio-file",
"/tmp/input.ogg",
"--retry-last",
"--pick",
"--replace-last-paste",
"--target-window",
"0x1234",
]
);
assert_eq!(args.iter().filter(|arg| *arg == "--daemon").count(), 1);
assert!(!args.iter().any(|arg| arg == "--toggle"));
}
}