Skip to content

Commit f2701f8

Browse files
committed
fix: enable runtime hotkey updates without app restart
- Add missing global-shortcut permissions to capability files - Fix GeneralSettings to call set_global_shortcut when hotkey changes - Remove pending shortcut mechanism in favor of immediate application - Improve shortcut registration to only unregister specific old shortcut - Add proper error handling and user feedback via toasts - Clean up excessive logging while keeping essential info Fixes issue where users had to restart app after changing hotkeys on Windows and macOS. BREAKING CHANGE: Removed apply_pending_shortcut command (was internal only)
1 parent d2c6019 commit f2701f8

File tree

10 files changed

+191
-244
lines changed

10 files changed

+191
-244
lines changed

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"WebSearch",
1717
"Bash(gh pr diff:*)"
1818
],
19-
"deny": []
19+
"deny": [],
20+
"defaultMode": "acceptEdits"
2021
},
2122
"hooks": {
2223
"PreToolUse": [

src-tauri/capabilities/default.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
"opener:default",
2323
"store:default",
2424
"global-shortcut:default",
25+
"global-shortcut:allow-unregister-all",
26+
"global-shortcut:allow-register",
27+
"global-shortcut:allow-unregister",
28+
"global-shortcut:allow-is-registered",
2529
"dialog:default",
2630
"fs:scope-appdata",
2731
"shell:allow-open",

src-tauri/capabilities/macos.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
"opener:default",
2424
"store:default",
2525
"global-shortcut:default",
26+
"global-shortcut:allow-unregister-all",
27+
"global-shortcut:allow-register",
28+
"global-shortcut:allow-unregister",
29+
"global-shortcut:allow-is-registered",
2630
"dialog:default",
2731
"fs:scope-appdata",
2832
"shell:allow-open",

src-tauri/capabilities/windows.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
"opener:default",
2424
"store:default",
2525
"global-shortcut:default",
26+
"global-shortcut:allow-unregister-all",
27+
"global-shortcut:allow-register",
28+
"global-shortcut:allow-unregister",
29+
"global-shortcut:allow-is-registered",
2630
"dialog:default",
2731
"fs:scope-appdata",
2832
"shell:allow-open",

src-tauri/src/commands/audio.rs

Lines changed: 52 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,7 @@ pub async fn stop_recording(
526526
};
527527

528528
// === AUDIO VALIDATION - Check quality before transcription ===
529+
// Important: Keep pill window visible during validation for feedback
529530
let validation_start = Instant::now();
530531
log_start("AUDIO_VALIDATION");
531532
log_with_context(log::Level::Debug, "Validating audio", &[
@@ -560,26 +561,27 @@ pub async fn stop_recording(
560561
log::warn!("Failed to remove silent audio file: {}", e);
561562
}
562563

563-
// Emit event to show user feedback
564+
// Emit to pill window for immediate user feedback
564565
let _ = emit_to_window(
565566
&app,
566-
"main",
567-
"no-speech-detected",
568-
serde_json::json!({
569-
"title": "No Speech Detected",
570-
"message": "The recording appears to be silent. Please check your microphone and speak clearly.",
571-
"severity": "warning",
572-
"actions": ["retry", "settings"]
573-
}),
567+
"pill",
568+
"transcription-empty",
569+
"No speech detected"
574570
);
575571

576-
// Hide pill window
577-
if let Err(e) = crate::commands::window::hide_pill_widget(app.clone()).await {
578-
log::error!("Failed to hide pill window: {}", e);
579-
}
580-
581-
// Transition back to Idle
582-
update_recording_state(&app, RecordingState::Idle, None);
572+
// Wait for feedback to show before hiding pill
573+
let app_for_hide = app.clone();
574+
tokio::spawn(async move {
575+
tokio::time::sleep(std::time::Duration::from_millis(2000)).await;
576+
577+
// Hide pill window
578+
if let Err(e) = crate::commands::window::hide_pill_widget(app_for_hide.clone()).await {
579+
log::error!("Failed to hide pill window: {}", e);
580+
}
581+
582+
// Transition back to Idle
583+
update_recording_state(&app_for_hide, RecordingState::Idle, None);
584+
});
583585

584586
return Ok("".to_string()); // Don't proceed to transcription
585587
}
@@ -591,26 +593,27 @@ pub async fn stop_recording(
591593
log::warn!("Failed to remove quiet audio file: {}", e);
592594
}
593595

594-
// Emit event with specific guidance
596+
// Emit to pill window for immediate user feedback
595597
let _ = emit_to_window(
596598
&app,
597-
"main",
598-
"no-speech-detected",
599-
serde_json::json!({
600-
"title": "Audio Too Quiet",
601-
"message": suggestion,
602-
"severity": "warning",
603-
"actions": ["retry", "settings"]
604-
}),
599+
"pill",
600+
"transcription-empty",
601+
"Audio too quiet - please speak louder"
605602
);
606603

607-
// Hide pill window
608-
if let Err(e) = crate::commands::window::hide_pill_widget(app.clone()).await {
609-
log::error!("Failed to hide pill window: {}", e);
610-
}
611-
612-
// Transition back to Idle
613-
update_recording_state(&app, RecordingState::Idle, None);
604+
// Wait for feedback to show before hiding pill
605+
let app_for_hide = app.clone();
606+
tokio::spawn(async move {
607+
tokio::time::sleep(std::time::Duration::from_millis(2000)).await;
608+
609+
// Hide pill window
610+
if let Err(e) = crate::commands::window::hide_pill_widget(app_for_hide.clone()).await {
611+
log::error!("Failed to hide pill window: {}", e);
612+
}
613+
614+
// Transition back to Idle
615+
update_recording_state(&app_for_hide, RecordingState::Idle, None);
616+
});
614617

615618
return Ok("".to_string()); // Don't proceed to transcription
616619
}
@@ -622,26 +625,27 @@ pub async fn stop_recording(
622625
log::warn!("Failed to remove short audio file: {}", e);
623626
}
624627

625-
// Emit event
628+
// Emit to pill window for immediate user feedback
626629
let _ = emit_to_window(
627630
&app,
628-
"main",
629-
"no-speech-detected",
630-
serde_json::json!({
631-
"title": "Recording Too Short",
632-
"message": format!("Recording was only {:.1}s. Please hold the recording button longer and speak clearly.", duration),
633-
"severity": "warning",
634-
"actions": ["retry"]
635-
}),
631+
"pill",
632+
"transcription-empty",
633+
format!("Recording too short ({:.1}s)", duration)
636634
);
637635

638-
// Hide pill window
639-
if let Err(e) = crate::commands::window::hide_pill_widget(app.clone()).await {
640-
log::error!("Failed to hide pill window: {}", e);
641-
}
642-
643-
// Transition back to Idle
644-
update_recording_state(&app, RecordingState::Idle, None);
636+
// Wait for feedback to show before hiding pill
637+
let app_for_hide = app.clone();
638+
tokio::spawn(async move {
639+
tokio::time::sleep(std::time::Duration::from_millis(2000)).await;
640+
641+
// Hide pill window
642+
if let Err(e) = crate::commands::window::hide_pill_widget(app_for_hide.clone()).await {
643+
log::error!("Failed to hide pill window: {}", e);
644+
}
645+
646+
// Transition back to Idle
647+
update_recording_state(&app_for_hide, RecordingState::Idle, None);
648+
});
645649

646650
return Ok("".to_string()); // Don't proceed to transcription
647651
}
@@ -956,38 +960,6 @@ pub async fn stop_recording(
956960

957961
log::debug!("Transcription successful, {} chars", text.len());
958962

959-
// Check if transcription is empty or only whitespace
960-
if text.trim().is_empty() {
961-
log::info!("Transcription is empty, no speech detected");
962-
963-
// Emit event to pill for user feedback
964-
let _ = emit_to_window(
965-
&app_for_task,
966-
"pill",
967-
"transcription-empty",
968-
"No speech detected",
969-
);
970-
971-
// Wait a bit for feedback to show
972-
let app_for_empty = app_for_task.clone();
973-
tokio::spawn(async move {
974-
tokio::time::sleep(std::time::Duration::from_millis(2000)).await;
975-
976-
// Hide pill window
977-
let app_state = app_for_empty.state::<AppState>();
978-
if let Some(window_manager) = app_state.get_window_manager() {
979-
if let Err(e) = window_manager.hide_pill_window().await {
980-
log::error!("Failed to hide pill window: {}", e);
981-
}
982-
}
983-
984-
// Transition to idle state
985-
update_recording_state(&app_for_empty, RecordingState::Idle, None);
986-
});
987-
988-
return;
989-
}
990-
991963
// Check if AI enhancement is enabled BEFORE spawning task
992964
let ai_enabled = match app_for_task.store("settings") {
993965
Ok(store) => store

0 commit comments

Comments
 (0)