Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 32 additions & 6 deletions src-tauri/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use crate::audio_toolkit::is_microphone_access_denied;
use crate::managers::audio::AudioRecordingManager;
use crate::managers::history::HistoryManager;
use crate::managers::transcription::TranscriptionManager;
use crate::settings::{get_settings, AppSettings, APPLE_INTELLIGENCE_PROVIDER_ID};
use crate::settings::{
get_settings, AppSettings, TranscriptionContext, APPLE_INTELLIGENCE_PROVIDER_ID,
};
use crate::shortcut;
use crate::tray::{change_tray_icon, TrayIconState};
use crate::utils::{
Expand Down Expand Up @@ -41,7 +43,13 @@ impl Drop for FinishGuard {
// Shortcut Action Trait
pub trait ShortcutAction: Send + Sync {
fn start(&self, app: &AppHandle, binding_id: &str, shortcut_str: &str);
fn stop(&self, app: &AppHandle, binding_id: &str, shortcut_str: &str);
fn stop(
&self,
app: &AppHandle,
binding_id: &str,
shortcut_str: &str,
context: TranscriptionContext,
);
}

// Transcribe Action
Expand Down Expand Up @@ -409,7 +417,13 @@ impl ShortcutAction for TranscribeAction {
);
}

fn stop(&self, app: &AppHandle, binding_id: &str, _shortcut_str: &str) {
fn stop(
&self,
app: &AppHandle,
binding_id: &str,
_shortcut_str: &str,
context: TranscriptionContext,
) {
// Unregister the cancel shortcut when transcription stops
shortcut::unregister_cancel_shortcut(app);

Expand Down Expand Up @@ -521,7 +535,7 @@ impl ShortcutAction for TranscribeAction {
let ah_clone = ah.clone();
let paste_time = Instant::now();
ah.run_on_main_thread(move || {
match utils::paste(final_text, ah_clone.clone()) {
match utils::paste(final_text, ah_clone.clone(), context) {
Ok(()) => debug!(
"Text pasted successfully in {:?}",
paste_time.elapsed()
Expand Down Expand Up @@ -570,7 +584,13 @@ impl ShortcutAction for CancelAction {
utils::cancel_current_operation(app);
}

fn stop(&self, _app: &AppHandle, _binding_id: &str, _shortcut_str: &str) {
fn stop(
&self,
_app: &AppHandle,
_binding_id: &str,
_shortcut_str: &str,
_context: TranscriptionContext,
) {
// Nothing to do on stop for cancel
}
}
Expand All @@ -588,7 +608,13 @@ impl ShortcutAction for TestAction {
);
}

fn stop(&self, app: &AppHandle, binding_id: &str, shortcut_str: &str) {
fn stop(
&self,
app: &AppHandle,
binding_id: &str,
shortcut_str: &str,
_context: TranscriptionContext,
) {
log::info!(
"Shortcut ID '{}': Stopped - {} (App: {})", // Changed "Released" to "Stopped" for consistency
binding_id,
Expand Down
4 changes: 4 additions & 0 deletions src-tauri/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ pub struct CliArgs {
#[arg(long)]
pub cancel: bool,

/// Force auto-submit after transcription (sent to running instance)
#[arg(long, value_parser = ["enter", "ctrl+enter", "cmd+enter"], default_missing_value = "enter", num_args = 0..=1)]
pub auto_submit: Option<String>,

/// Enable debug mode with verbose logging
#[arg(long)]
pub debug: bool,
Expand Down
19 changes: 15 additions & 4 deletions src-tauri/src/clipboard.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use crate::input::{self, EnigoState};
#[cfg(target_os = "linux")]
use crate::settings::TypingTool;
use crate::settings::{get_settings, AutoSubmitKey, ClipboardHandling, PasteMethod};
use crate::settings::{
get_settings, AutoSubmitKey, ClipboardHandling, PasteMethod, TranscriptionContext,
};
use enigo::{Direction, Enigo, Key, Keyboard};
use log::info;
use std::process::Command;
Expand Down Expand Up @@ -588,7 +590,11 @@ fn should_send_auto_submit(auto_submit: bool, paste_method: PasteMethod) -> bool
auto_submit && paste_method != PasteMethod::None
}

pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> {
pub fn paste(
text: String,
app_handle: AppHandle,
context: TranscriptionContext,
) -> Result<(), String> {
let settings = get_settings(&app_handle);
let paste_method = settings.paste_method;
let paste_delay_ms = settings.paste_delay_ms;
Expand Down Expand Up @@ -646,9 +652,14 @@ pub fn paste(text: String, app_handle: AppHandle) -> Result<(), String> {
}
}

if should_send_auto_submit(settings.auto_submit, paste_method) {
let (auto_submit, auto_submit_key) = match context.auto_submit_override {
Some(key) => (true, key),
None => (settings.auto_submit, settings.auto_submit_key),
};

if should_send_auto_submit(auto_submit, paste_method) {
std::thread::sleep(Duration::from_millis(50));
send_return_key(&mut enigo, settings.auto_submit_key)?;
send_return_key(&mut enigo, auto_submit_key)?;
Comment on lines +655 to +662
}

// After pasting, optionally copy to clipboard based on settings
Expand Down
26 changes: 24 additions & 2 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -472,10 +472,32 @@ pub fn run(cli_args: CliArgs) {

builder
.plugin(tauri_plugin_single_instance::init(|app, args, _cwd| {
let context = {
let mut ctx = settings::TranscriptionContext::default();
if let Some(pos) = args.iter().position(|a| a == "--auto-submit") {
let key = args
.get(pos + 1)
.filter(|s| !s.starts_with("--"))
.map(|s| s.as_str())
.unwrap_or("enter");
Comment on lines +477 to +482
ctx.auto_submit_override = Some(match key {
"ctrl+enter" => settings::AutoSubmitKey::CtrlEnter,
"cmd+enter" => settings::AutoSubmitKey::CmdEnter,
_ => settings::AutoSubmitKey::Enter,
});
}
ctx
};

if args.iter().any(|a| a == "--toggle-transcription") {
signal_handle::send_transcription_input(app, "transcribe", "CLI");
signal_handle::send_transcription_input(app, "transcribe", "CLI", context);
} else if args.iter().any(|a| a == "--toggle-post-process") {
signal_handle::send_transcription_input(app, "transcribe_with_post_process", "CLI");
signal_handle::send_transcription_input(
app,
"transcribe_with_post_process",
"CLI",
context,
);
} else if args.iter().any(|a| a == "--cancel") {
crate::utils::cancel_current_operation(app);
} else {
Expand Down
10 changes: 10 additions & 0 deletions src-tauri/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,16 @@ impl Default for AutoSubmitKey {
}
}

/// Per-invocation overrides for transcription behavior.
/// Passed through the pipeline from CLI/signal triggers to the paste step.
/// Fields are `Option` — `None` means "use the persistent setting".
#[derive(Debug, Clone, Default)]
pub struct TranscriptionContext {
/// `None` = use persistent `auto_submit` + `auto_submit_key` settings.
/// `Some(key)` = force auto-submit with the given key.
pub auto_submit_override: Option<AutoSubmitKey>,
}

impl ModelUnloadTimeout {
pub fn to_minutes(self) -> Option<u64> {
match self {
Expand Down
17 changes: 14 additions & 3 deletions src-tauri/src/shortcut/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use tauri::{AppHandle, Manager};

use crate::actions::ACTION_MAP;
use crate::managers::audio::AudioRecordingManager;
use crate::settings::get_settings;
use crate::settings::{get_settings, TranscriptionContext};
use crate::transcription_coordinator::is_transcribe_binding;
use crate::TranscriptionCoordinator;

Expand Down Expand Up @@ -37,7 +37,13 @@ pub fn handle_shortcut_event(
// Transcribe bindings are handled by the coordinator.
if is_transcribe_binding(binding_id) {
if let Some(coordinator) = app.try_state::<TranscriptionCoordinator>() {
coordinator.send_input(binding_id, hotkey_string, is_pressed, settings.push_to_talk);
coordinator.send_input(
binding_id,
hotkey_string,
is_pressed,
settings.push_to_talk,
TranscriptionContext::default(),
);
} else {
warn!("TranscriptionCoordinator is not initialized");
}
Expand Down Expand Up @@ -65,6 +71,11 @@ pub fn handle_shortcut_event(
if is_pressed {
action.start(app, binding_id, hotkey_string);
} else {
action.stop(app, binding_id, hotkey_string);
action.stop(
app,
binding_id,
hotkey_string,
TranscriptionContext::default(),
);
}
}
17 changes: 14 additions & 3 deletions src-tauri/src/signal_handle.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::settings::TranscriptionContext;
use crate::TranscriptionCoordinator;
#[cfg(unix)]
use log::debug;
Expand All @@ -13,9 +14,14 @@ use std::thread;

/// Send a transcription input to the coordinator.
/// Used by signal handlers, CLI flags, and any other external trigger.
pub fn send_transcription_input(app: &AppHandle, binding_id: &str, source: &str) {
pub fn send_transcription_input(
app: &AppHandle,
binding_id: &str,
source: &str,
context: TranscriptionContext,
) {
if let Some(c) = app.try_state::<TranscriptionCoordinator>() {
c.send_input(binding_id, source, true, false);
c.send_input(binding_id, source, true, false, context);
} else {
warn!("TranscriptionCoordinator not initialized");
}
Expand All @@ -32,7 +38,12 @@ pub fn setup_signal_handler(app_handle: AppHandle, mut signals: Signals) {
_ => continue,
};
debug!("Received {signal_name}");
send_transcription_input(&app_handle, binding_id, signal_name);
send_transcription_input(
&app_handle,
binding_id,
signal_name,
TranscriptionContext::default(),
);
}
});
}
59 changes: 47 additions & 12 deletions src-tauri/src/transcription_coordinator.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::actions::ACTION_MAP;
use crate::managers::audio::AudioRecordingManager;
use crate::settings::TranscriptionContext;
use log::{debug, error, warn};
use std::sync::mpsc::{self, Sender};
use std::sync::Arc;
Expand All @@ -16,6 +17,7 @@ enum Command {
hotkey_string: String,
is_pressed: bool,
push_to_talk: bool,
context: TranscriptionContext,
},
Cancel {
recording_was_active: bool,
Expand All @@ -26,7 +28,7 @@ enum Command {
/// Pipeline lifecycle, owned exclusively by the coordinator thread.
enum Stage {
Idle,
Recording(String), // binding_id
Recording(String, TranscriptionContext),
Processing,
}

Expand Down Expand Up @@ -57,6 +59,7 @@ impl TranscriptionCoordinator {
hotkey_string,
is_pressed,
push_to_talk,
context,
} => {
// Debounce rapid-fire press events (key repeat / double-tap).
// Releases always pass through for push-to-talk.
Expand All @@ -71,19 +74,27 @@ impl TranscriptionCoordinator {

if push_to_talk {
if is_pressed && matches!(stage, Stage::Idle) {
start(&app, &mut stage, &binding_id, &hotkey_string);
start(&app, &mut stage, &binding_id, &hotkey_string, context);
} else if !is_pressed
&& matches!(&stage, Stage::Recording(id) if id == &binding_id)
&& matches!(&stage, Stage::Recording(ref id, _) if id == &binding_id)
{
stop(&app, &mut stage, &binding_id, &hotkey_string);
let ctx = take_recording_context(&mut stage);
stop(&app, &mut stage, &binding_id, &hotkey_string, ctx);
}
} else if is_pressed {
match &stage {
Stage::Idle => {
start(&app, &mut stage, &binding_id, &hotkey_string);
start(
&app,
&mut stage,
&binding_id,
&hotkey_string,
context,
);
}
Stage::Recording(id) if id == &binding_id => {
stop(&app, &mut stage, &binding_id, &hotkey_string);
Stage::Recording(id, _) if id == &binding_id => {
let ctx = take_recording_context(&mut stage);
stop(&app, &mut stage, &binding_id, &hotkey_string, ctx);
Comment on lines +95 to +97
}
_ => {
debug!("Ignoring press for '{binding_id}': pipeline busy")
Expand All @@ -96,7 +107,7 @@ impl TranscriptionCoordinator {
} => {
// Don't reset during processing — wait for the pipeline to finish.
if !matches!(stage, Stage::Processing)
&& (recording_was_active || matches!(stage, Stage::Recording(_)))
&& (recording_was_active || matches!(stage, Stage::Recording(_, _)))
{
stage = Stage::Idle;
}
Expand Down Expand Up @@ -124,6 +135,7 @@ impl TranscriptionCoordinator {
hotkey_string: &str,
is_pressed: bool,
push_to_talk: bool,
context: TranscriptionContext,
) {
if self
.tx
Expand All @@ -132,6 +144,7 @@ impl TranscriptionCoordinator {
hotkey_string: hotkey_string.to_string(),
is_pressed,
push_to_talk,
context,
})
.is_err()
{
Expand All @@ -158,7 +171,23 @@ impl TranscriptionCoordinator {
}
}

fn start(app: &AppHandle, stage: &mut Stage, binding_id: &str, hotkey_string: &str) {
fn take_recording_context(stage: &mut Stage) -> TranscriptionContext {
match std::mem::replace(stage, Stage::Processing) {
Stage::Recording(_, ctx) => ctx,
other => {
*stage = other;
TranscriptionContext::default()
}
}
}

fn start(
app: &AppHandle,
stage: &mut Stage,
binding_id: &str,
hotkey_string: &str,
context: TranscriptionContext,
) {
let Some(action) = ACTION_MAP.get(binding_id) else {
warn!("No action in ACTION_MAP for '{binding_id}'");
return;
Expand All @@ -168,17 +197,23 @@ fn start(app: &AppHandle, stage: &mut Stage, binding_id: &str, hotkey_string: &s
.try_state::<Arc<AudioRecordingManager>>()
.map_or(false, |a| a.is_recording())
{
*stage = Stage::Recording(binding_id.to_string());
*stage = Stage::Recording(binding_id.to_string(), context);
} else {
debug!("Start for '{binding_id}' did not begin recording; staying idle");
}
}

fn stop(app: &AppHandle, stage: &mut Stage, binding_id: &str, hotkey_string: &str) {
fn stop(
app: &AppHandle,
stage: &mut Stage,
binding_id: &str,
hotkey_string: &str,
context: TranscriptionContext,
) {
let Some(action) = ACTION_MAP.get(binding_id) else {
warn!("No action in ACTION_MAP for '{binding_id}'");
return;
};
action.stop(app, binding_id, hotkey_string);
action.stop(app, binding_id, hotkey_string, context);
*stage = Stage::Processing;
}
Loading