diff --git a/src-tauri/Info.plist b/src-tauri/Info.plist new file mode 100644 index 0000000..83deebc --- /dev/null +++ b/src-tauri/Info.plist @@ -0,0 +1,8 @@ + + + + + NSMicrophoneUsageDescription + VibeToText needs microphone access to capture speech for local dictation. + + diff --git a/src-tauri/src/audio.rs b/src-tauri/src/audio.rs index 8fa0b16..f09a15a 100644 --- a/src-tauri/src/audio.rs +++ b/src-tauri/src/audio.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Result}; use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use crossbeam_channel::{bounded, Receiver, Sender}; +use serde::Serialize; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -16,6 +17,13 @@ pub struct Frame(pub Vec); #[derive(Clone, Copy, Debug)] pub struct Level(pub f32); +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct InputDeviceInfo { + pub name: String, + pub is_default: bool, +} + pub struct AudioCapture { stop: Arc, pub frames: Receiver, @@ -75,6 +83,27 @@ impl AudioCapture { } } +pub fn input_devices() -> Result> { + let host = cpal::default_host(); + let default_name = host.default_input_device().and_then(|d| d.name().ok()); + let mut devices = Vec::new(); + + for device in host.input_devices()? { + let Ok(name) = device.name() else { + continue; + }; + let is_default = default_name.as_ref() == Some(&name); + devices.push(InputDeviceInfo { name, is_default }); + } + + devices.sort_by(|a, b| match (a.is_default, b.is_default) { + (true, false) => std::cmp::Ordering::Less, + (false, true) => std::cmp::Ordering::Greater, + _ => a.name.to_lowercase().cmp(&b.name.to_lowercase()), + }); + Ok(devices) +} + fn run_stream( device: cpal::Device, config: cpal::SupportedStreamConfig, diff --git a/src-tauri/src/inject.rs b/src-tauri/src/inject.rs index f9704d4..3df3729 100644 --- a/src-tauri/src/inject.rs +++ b/src-tauri/src/inject.rs @@ -1,5 +1,7 @@ use anyhow::Result; -use enigo::{Direction, Enigo, Key, Keyboard, Settings}; +#[cfg(not(target_os = "macos"))] +use enigo::Key; +use enigo::{Direction, Enigo, Keyboard, Settings}; use tauri::AppHandle; use tauri_plugin_clipboard_manager::ClipboardExt; @@ -7,7 +9,7 @@ use tauri_plugin_clipboard_manager::ClipboardExt; /// Used in stream mode for incremental partials. #[allow(dead_code)] pub fn type_text(s: &str) -> Result<()> { - let mut enigo = Enigo::new(&Settings::default())?; + let mut enigo = new_enigo()?; enigo.text(s)?; Ok(()) } @@ -74,15 +76,7 @@ pub fn paste_text(app: &AppHandle, s: &str) -> Result<()> { ); } - let mut enigo = Enigo::new(&Settings::default())?; - #[cfg(target_os = "macos")] - let mod_key = Key::Meta; - #[cfg(not(target_os = "macos"))] - let mod_key = Key::Control; - - enigo.key(mod_key, Direction::Press)?; - enigo.key(Key::Unicode('v'), Direction::Click)?; - enigo.key(mod_key, Direction::Release)?; + send_paste_shortcut()?; // Restore the user's previous clipboard contents. CRITICAL: there's // no API for "the foreground app finished consuming this paste," @@ -136,3 +130,33 @@ pub fn paste_text(app: &AppHandle, s: &str) -> Result<()> { } Ok(()) } + +#[cfg(target_os = "macos")] +fn send_paste_shortcut() -> Result<()> { + let mut enigo = new_enigo()?; + // macOS keycode 55 = Command, 9 = V. Using raw keycodes avoids Enigo's + // layout lookup path, which must run on the main dispatch queue. + enigo.raw(55, Direction::Press)?; + enigo.raw(9, Direction::Click)?; + enigo.raw(55, Direction::Release)?; + Ok(()) +} + +#[cfg(not(target_os = "macos"))] +fn send_paste_shortcut() -> Result<()> { + let mut enigo = new_enigo()?; + enigo.key(Key::Control, Direction::Press)?; + enigo.key(Key::Unicode('v'), Direction::Click)?; + enigo.key(Key::Control, Direction::Release)?; + Ok(()) +} + +fn new_enigo() -> Result { + // The app asks for Accessibility separately. During dictation, repeatedly + // opening the system prompt is noisy and can steal focus from the target app. + let settings = Settings { + open_prompt_to_get_permissions: false, + ..Settings::default() + }; + Ok(Enigo::new(&settings)?) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index dc09400..ffa0d24 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -69,6 +69,11 @@ async fn get_config(state: tauri::State<'_, Arc>) -> Result Result, String> { + audio::input_devices().map_err(|e| e.to_string()) +} + #[tauri::command] async fn save_config( app: tauri::AppHandle, @@ -1176,6 +1181,7 @@ pub fn run() { }) .invoke_handler(tauri::generate_handler![ get_config, + list_input_devices, save_config, toggle_dictation, current_backend, diff --git a/src/index.html b/src/index.html index e8efaee..842aeb8 100644 --- a/src/index.html +++ b/src/index.html @@ -172,8 +172,11 @@

Behavior

+

Microphones: checking…