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…