diff --git a/src-tauri/src/audio/recorder.rs b/src-tauri/src/audio/recorder.rs index 0abca67..c6942e7 100644 --- a/src-tauri/src/audio/recorder.rs +++ b/src-tauri/src/audio/recorder.rs @@ -365,10 +365,10 @@ impl AudioRecorder { } pub fn check_dependencies() -> (bool, String) { - if crate::utils::swift_binary::ensure_swift_binary("record", "scripts/record.swift") { + if crate::utils::swift_binary::get_binary_path("record").exists() { (true, "native audio recorder ready".into()) } else { - (false, "Audio recorder unavailable. Install Xcode Command Line Tools: xcode-select --install".into()) + (false, "Audio recorder binary not found. Reinstall Echo or install Xcode Command Line Tools: xcode-select --install".into()) } } diff --git a/src-tauri/src/transcription/whisper.rs b/src-tauri/src/transcription/whisper.rs index 54a621f..ef8bf00 100644 --- a/src-tauri/src/transcription/whisper.rs +++ b/src-tauri/src/transcription/whisper.rs @@ -395,6 +395,13 @@ pub async fn build_binary(progress_cb: impl Fn(&str) + Send + 'static) -> Result return Ok(()); } + if !cmd_exists("git") { + return Err("git is not installed. Install Xcode Command Line Tools: xcode-select --install".into()); + } + if !cmd_exists("cmake") { + return Err("cmake is not installed. Install it with: brew install cmake".into()); + } + let tmp_dir = std::env::temp_dir().join("echo-whisper-build"); fs::create_dir_all(bin_dir()).map_err(|e| e.to_string())?; fs::create_dir_all(&tmp_dir).map_err(|e| e.to_string())?; @@ -443,15 +450,27 @@ pub async fn build_binary(progress_cb: impl Fn(&str) + Send + 'static) -> Result Ok(()) } -fn run_cmd(cmd: &str, args: &[&str], cwd: &Path) -> Result<(), String> { - let path_env = format!( +fn build_path_env() -> String { + format!( "{}:/opt/homebrew/bin:/usr/local/bin", std::env::var("PATH").unwrap_or_default() - ); + ) +} + +fn cmd_exists(name: &str) -> bool { + Command::new(name) + .arg("--version") + .env("PATH", build_path_env()) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) +} + +fn run_cmd(cmd: &str, args: &[&str], cwd: &Path) -> Result<(), String> { let output = Command::new(cmd) .args(args) .current_dir(cwd) - .env("PATH", &path_env) + .env("PATH", build_path_env()) .output() .map_err(|e| format!("{} failed: {}", cmd, e))?; diff --git a/src-tauri/src/utils/errors.rs b/src-tauri/src/utils/errors.rs index a0268f9..60558f5 100644 --- a/src-tauri/src/utils/errors.rs +++ b/src-tauri/src/utils/errors.rs @@ -2,7 +2,10 @@ pub fn to_user_facing_error(err: &str) -> String { let lower = err.to_lowercase(); if lower.contains("audio recorder") || lower.contains("record.swift") || lower.contains("rec: command not found") { - return "Audio recording is unavailable. Grant microphone access in System Settings → Privacy & Security → Microphone (and install Xcode Command Line Tools if prompted).".into(); + if lower.contains("unavailable") || lower.contains("not found") || lower.contains("command not found") || lower.contains("no such file") { + return "Audio recorder binary is missing. Reinstall Echo or install Xcode Command Line Tools (xcode-select --install) and restart.".into(); + } + return "Audio recording failed. Grant microphone access in System Settings → Privacy & Security → Microphone.".into(); } if lower.contains("whisper") && (lower.contains("not found") || lower.contains("not ready")) { return "Whisper is not set up. Open Settings and build/download Whisper.".into(); @@ -47,8 +50,27 @@ mod tests { // Mirrors tests/errors.test.ts. The TS side wraps input in `new Error(...)` // and extracts `.message`; the Rust API takes the message string directly. #[test] - fn maps_audio_recorder_errors() { - assert!(to_user_facing_error("rec: command not found").contains("Audio recording")); + fn maps_audio_recorder_missing_binary() { + let result = to_user_facing_error("rec: command not found"); + assert!(result.contains("Audio recorder binary is missing"), "got: {}", result); + } + + #[test] + fn maps_audio_recorder_binary_unavailable() { + let result = to_user_facing_error("Native audio recorder unavailable (failed to compile record.swift)"); + assert!(result.contains("missing"), "got: {}", result); + } + + #[test] + fn maps_audio_recorder_spawn_no_such_file() { + let result = to_user_facing_error("Failed to start native audio recorder: No such file or directory (os error 2)"); + assert!(result.contains("missing"), "got: {}", result); + } + + #[test] + fn maps_audio_recorder_permission_errors() { + let result = to_user_facing_error("audio recorder permission denied"); + assert!(result.contains("Microphone"), "got: {}", result); } #[test] diff --git a/src-tauri/src/utils/swift_binary.rs b/src-tauri/src/utils/swift_binary.rs index 04b9ebd..cb0f39d 100644 --- a/src-tauri/src/utils/swift_binary.rs +++ b/src-tauri/src/utils/swift_binary.rs @@ -171,7 +171,7 @@ pub fn ensure_swift_binary(binary_name: &str, source_relative_path: &str) -> boo Some(p) => p, None => { if binary_path.exists() { return true; } - log::warn!("[swift-binary] Source not found for {}", binary_name); + log::debug!("[swift-binary] Source not found for {}", binary_name); return false; } };