From cff0969b36393b6b66acc5d1af48043f1c9a001c Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Mon, 12 May 2025 14:09:47 -0700 Subject: [PATCH] add Jon's feature. --- src-tauri/resources/tray_32x32.png | Bin 469 -> 0 bytes .../{tray_64x64.png => tray_idle.png} | Bin src-tauri/src/actions.rs | 34 +++---------- src-tauri/src/lib.rs | 17 +++++-- src-tauri/src/managers/transcription.rs | 1 + src-tauri/src/settings.rs | 32 ++++++------ src-tauri/src/shortcut.rs | 47 +++++++++++++++--- src-tauri/src/utils.rs | 26 ++++++++++ src/components/settings/KeyboardShortcuts.tsx | 26 ++++++++++ src/components/settings/Settings.tsx | 4 +- src/lib/types.ts | 1 + 11 files changed, 134 insertions(+), 54 deletions(-) delete mode 100644 src-tauri/resources/tray_32x32.png rename src-tauri/resources/{tray_64x64.png => tray_idle.png} (100%) diff --git a/src-tauri/resources/tray_32x32.png b/src-tauri/resources/tray_32x32.png deleted file mode 100644 index 63bafdeafd81dcb6b04069a025cbfcb828ed5325..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 469 zcmV;`0V@89P)l)S0aMTh*(M5@wrp; zvg!sai4o8+*_2{3?hwIMM6A5nVOXu92`}^_@SXnF+l{wZEQpG!fN)YtWux6;T$PQA z+0rZ$V9{?vz4<~yqsA32P+&MAv;3q2va6+pvf@W?9~9KmcT9Aom>vITnEZfWqK}nf z;yr~tao%zKreZ53qMR9vkR!zeYKkyQR1ymGr4NKM>k5<{w-cesu-+ap7PZH?Gwv{6 zby5M~%Zr8I^>LKA#ofP_N(); - tray.set_icon(Some( - Image::from_path( - app.path() - .resolve( - "resources/tray_recording.png", - tauri::path::BaseDirectory::Resource, - ) - .expect("failed to resolve"), - ) - .expect("failed to set icon"), - )); + change_tray_icon(app, TrayIconState::Recording); let rm = app.state::>(); rm.try_start_recording("transcribe"); } fn transcribe_released_action(app: &AppHandle, _shortcut_str: &str) { - let tray = app.state::(); - tray.set_icon(Some( - Image::from_path( - app.path() - .resolve( - "resources/tray_64x64.png", - tauri::path::BaseDirectory::Resource, - ) - .expect("failed to resolve"), - ) - .expect("failed to set icon"), - )); - let ah = app.clone(); let rm = Arc::clone(&app.state::>()); let tm = Arc::clone(&app.state::>()); + change_tray_icon(app, TrayIconState::Idle); + tauri::async_runtime::spawn(async move { if let Some(samples) = rm.stop_recording("transcribe") { match tm.transcribe(samples) { - // Not .await, as transcribe is synchronous Ok(transcription) => { println!("Global Shortcut Transcription: {}", transcription); utils::paste(transcription, ah); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 972b4c7..afe2d4f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -6,13 +6,22 @@ mod utils; use managers::audio::AudioRecordingManager; use managers::transcription::TranscriptionManager; -use std::sync::Arc; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; use tauri::image::Image; use tauri::menu::{Menu, MenuItem}; use tauri::tray::TrayIconBuilder; use tauri::Manager; use tauri_plugin_autostart::MacosLauncher; +#[derive(Default)] +struct ShortcutToggleStates { + // Map: shortcut_binding_id -> is_active + active_toggles: HashMap, +} + +type ManagedToggleState = Mutex; + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { env_logger::init(); @@ -27,13 +36,14 @@ pub fn run() { .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_store::Builder::default().build()) .plugin(tauri_plugin_global_shortcut::Builder::new().build()) + .manage(Mutex::new(ShortcutToggleStates::default())) .setup(move |app| { let settings_i = MenuItem::with_id(app, "settings", "Settings", true, None::<&str>)?; let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; let menu = Menu::with_items(app, &[&settings_i, &quit_i])?; let tray = TrayIconBuilder::new() .icon(Image::from_path(app.path().resolve( - "resources/tray_64x64.png", + "resources/tray_idle.png", tauri::path::BaseDirectory::Resource, )?)?) .menu(&menu) @@ -86,7 +96,8 @@ pub fn run() { }) .invoke_handler(tauri::generate_handler![ shortcut::change_binding, - shortcut::reset_binding + shortcut::reset_binding, + shortcut::change_ptt_setting ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/managers/transcription.rs b/src-tauri/src/managers/transcription.rs index f0ae2cd..83555fe 100644 --- a/src-tauri/src/managers/transcription.rs +++ b/src-tauri/src/managers/transcription.rs @@ -54,6 +54,7 @@ impl TranscriptionManager { params.set_print_progress(false); params.set_print_realtime(false); params.set_print_timestamps(false); + params.set_suppress_blank(true); params.set_suppress_non_speech_tokens(true); state diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index b9280c8..021c9be 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, sync::Arc}; -use tauri::{App, AppHandle, Runtime}; -use tauri_plugin_store::{Store, StoreExt}; +use std::collections::HashMap; +use tauri::{App, AppHandle}; +use tauri_plugin_store::StoreExt; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ShortcutBinding { @@ -16,6 +16,7 @@ pub struct ShortcutBinding { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct AppSettings { pub bindings: HashMap, + pub push_to_talk: bool, } pub const SETTINGS_STORE_PATH: &str = "settings_store.json"; @@ -32,18 +33,21 @@ pub fn get_default_settings() -> AppSettings { current_binding: "alt+space".to_string(), }, ); - bindings.insert( - "test".to_string(), - ShortcutBinding { - id: "test".to_string(), - name: "Test".to_string(), - description: "This is a test binding.".to_string(), - default_binding: "ctrl+d".to_string(), - current_binding: "ctrl+d".to_string(), - }, - ); + // bindings.insert( + // "test".to_string(), + // ShortcutBinding { + // id: "test".to_string(), + // name: "Test".to_string(), + // description: "This is a test binding.".to_string(), + // default_binding: "ctrl+d".to_string(), + // current_binding: "ctrl+d".to_string(), + // }, + // ); - AppSettings { bindings } + AppSettings { + bindings, + push_to_talk: true, + } } pub fn load_or_create_app_settings(app: &App) -> AppSettings { diff --git a/src-tauri/src/shortcut.rs b/src-tauri/src/shortcut.rs index 508ced6..5785563 100644 --- a/src-tauri/src/shortcut.rs +++ b/src-tauri/src/shortcut.rs @@ -1,11 +1,12 @@ use serde::Serialize; -use tauri::{App, AppHandle}; +use tauri::{App, AppHandle, Manager}; use tauri_plugin_global_shortcut::GlobalShortcutExt; use tauri_plugin_global_shortcut::{Shortcut, ShortcutState}; use crate::actions::ACTION_MAP; -use crate::settings; use crate::settings::ShortcutBinding; +use crate::settings::{self, get_settings}; +use crate::ManagedToggleState; pub fn init_shortcuts(app: &App) { let settings = settings::load_or_create_app_settings(app); @@ -89,6 +90,19 @@ pub fn reset_binding(app: AppHandle, id: String) -> Result Result<(), String> { + let mut settings = settings::get_settings(&app); + + // TODO if the setting is currently false, we probably want to + // cancel any ongoing recordings or actions + settings.push_to_talk = enabled; + + settings::write_settings(&app, settings); + + Ok(()) +} + fn _register_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), String> { // Parse shortcut and return error if it fails let shortcut = match binding.current_binding.parse::() { @@ -100,15 +114,34 @@ fn _register_shortcut(app: &AppHandle, binding: ShortcutBinding) -> Result<(), S let binding_id_for_closure = binding.id.clone(); app.global_shortcut() - .on_shortcut(shortcut, move |handler_app, scut, event| { + .on_shortcut(shortcut, move |ah, scut, event| { if scut == &shortcut { let shortcut_string = scut.into_string(); + let settings = get_settings(ah); if let Some(action_set) = ACTION_MAP.get(&binding_id_for_closure) { - if event.state == ShortcutState::Pressed { - (action_set.press)(handler_app, &shortcut_string); - } else if event.state == ShortcutState::Released { - (action_set.release)(handler_app, &shortcut_string); + if settings.push_to_talk { + if event.state == ShortcutState::Pressed { + (action_set.press)(ah, &shortcut_string); + } else if event.state == ShortcutState::Released { + (action_set.release)(ah, &shortcut_string); + } + } else { + if event.state == ShortcutState::Pressed { + let toggle_state_manager = ah.state::(); + + let mut states = toggle_state_manager.lock().expect("Failed to lock toggle state manager"); + + let is_currently_active = states.active_toggles.entry(binding_id_for_closure.clone()).or_insert(false); + + if *is_currently_active { + (action_set.release)(ah, &shortcut_string); + *is_currently_active = false; // Update state to inactive + } else { + (action_set.press)(ah, &shortcut_string); + *is_currently_active = true; // Update state to active + } + } } } else { println!( diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 2955600..a7261b4 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -1,7 +1,10 @@ use rdev::{simulate, EventType, Key, SimulateError}; use std::thread; use std::time; +use tauri::image::Image; +use tauri::tray::TrayIcon; use tauri::AppHandle; +use tauri::Manager; use tauri_plugin_clipboard_manager::ClipboardExt; fn try_send_event(event: &EventType) { @@ -43,3 +46,26 @@ pub fn paste(text: String, app_handle: AppHandle) { // restore the clipboard clipboard.write_text(&clipboard_content).unwrap(); } + +pub enum TrayIconState { + Idle, + Recording, +} + +pub fn change_tray_icon(app: &AppHandle, icon: TrayIconState) { + let tray = app.state::(); + + let icon_path = match icon { + TrayIconState::Idle => "resources/tray_idle.png", + TrayIconState::Recording => "resources/tray_recording.png", + }; + + let _ = tray.set_icon(Some( + Image::from_path( + app.path() + .resolve(icon_path, tauri::path::BaseDirectory::Resource) + .expect("failed to resolve"), + ) + .expect("failed to set icon"), + )); +} diff --git a/src/components/settings/KeyboardShortcuts.tsx b/src/components/settings/KeyboardShortcuts.tsx index 28b8f9a..dc36d03 100644 --- a/src/components/settings/KeyboardShortcuts.tsx +++ b/src/components/settings/KeyboardShortcuts.tsx @@ -12,6 +12,7 @@ import ResetIcon from "../icons/ResetIcon"; export const KeyboardShortcuts: React.FC = () => { const [bindings, setBindings] = React.useState({}); + const [pttEnabled, setPttEnabled] = React.useState(false); const [keyPressed, setKeyPressed] = useState([]); const [recordedKeys, setRecordedKeys] = useState([]); const [editingShortcutId, setEditingShortcutId] = useState( @@ -27,6 +28,7 @@ export const KeyboardShortcuts: React.FC = () => { r.get("settings").then((s) => { const settings = SettingsSchema.parse(s); setBindings(settings.bindings); + setPttEnabled(settings.push_to_talk); }); }); }, []); @@ -146,6 +148,30 @@ export const KeyboardShortcuts: React.FC = () => { return (
+
+
+

Push To Talk

+

Hold to record, release to stop

+
+ +
{Object.entries(bindings).map(([id, binding]) => (
{ return (
-
-

Keyboard Shortcuts

+
+ {/*

Keyboard Shortcuts

*/}
diff --git a/src/lib/types.ts b/src/lib/types.ts index 717a08e..08b810f 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -15,6 +15,7 @@ export const ShortcutBindingsMapSchema = z.record( export const SettingsSchema = z.object({ bindings: ShortcutBindingsMapSchema, + push_to_talk: z.boolean(), }); export const BindingResponseSchema = z.object({