feat: SIGUSR2 handling for toggles (#354)

* feat: SIGUSR2 handling for toggles

* run format

---------

Co-authored-by: CJ Pais <cj@cjpais.com>
This commit is contained in:
johnpyp 2025-11-18 21:45:46 -05:00 committed by GitHub
parent 97f3018be0
commit 9c4b1e1f5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 95 additions and 9 deletions

11
src-tauri/Cargo.lock generated
View file

@ -2349,6 +2349,7 @@ dependencies = [
"rustfft",
"serde",
"serde_json",
"signal-hook",
"strsim",
"tar",
"tauri",
@ -5560,6 +5561,16 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
dependencies = [
"libc",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.6"

View file

@ -29,10 +29,10 @@ tauri-build = { version = "2", features = [] }
[dependencies]
once_cell = "1"
tauri = { version = "2.9.1", features = [
"protocol-asset",
"macos-private-api",
"tray-icon",
'image-png',
"protocol-asset",
"macos-private-api",
"tray-icon",
'image-png',
] }
tauri-plugin-log = "2.7.1"
tauri-plugin-opener = "2.5.2"
@ -68,6 +68,9 @@ tar = "0.4.44"
flate2 = "1.0"
transcribe-rs = "0.1.4"
[target.'cfg(unix)'.dependencies]
signal-hook = "0.3"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-autostart = "2.5.1"
tauri-plugin-global-shortcut = "2.3.1"
@ -76,9 +79,9 @@ tauri-plugin-updater = "2.9.0"
[target.'cfg(windows)'.dependencies]
windows = { version = "0.61.3", features = [
"Win32_Media_Audio_Endpoints",
"Win32_System_Com_StructuredStorage",
"Win32_System_Variant",
"Win32_Media_Audio_Endpoints",
"Win32_System_Com_StructuredStorage",
"Win32_System_Variant",
] }
[profile.release]

View file

@ -38,9 +38,9 @@ pub fn is_laptop() -> Result<bool, String> {
.arg("batt")
.output()
.map_err(|e| e.to_string())?;
let stdout = String::from_utf8_lossy(&output.stdout);
// Check if InternalBattery is present (laptops have batteries, desktops typically don't)
Ok(stdout.contains("InternalBattery"))
}

View file

@ -9,6 +9,7 @@ mod managers;
mod overlay;
mod settings;
mod shortcut;
mod signal_handle;
mod tray;
mod utils;
@ -17,6 +18,8 @@ use managers::audio::AudioRecordingManager;
use managers::history::HistoryManager;
use managers::model::ModelManager;
use managers::transcription::TranscriptionManager;
use signal_hook::consts::SIGUSR2;
use signal_hook::iterator::Signals;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::{Arc, Mutex};
@ -119,6 +122,12 @@ fn initialize_core_logic(app_handle: &AppHandle) {
// Initialize the shortcuts
shortcut::init_shortcuts(app_handle);
#[cfg(unix)]
let signals = Signals::new(&[SIGUSR2]).unwrap();
// Set up SIGUSR2 signal handler for toggling transcription
#[cfg(unix)]
signal_handle::setup_signal_handler(app_handle.clone(), signals);
// Apply macOS Accessory policy if starting hidden
#[cfg(target_os = "macos")]
{

View file

@ -0,0 +1,63 @@
use crate::actions::ACTION_MAP;
use crate::ManagedToggleState;
use log::{debug, info, warn};
use std::thread;
use tauri::{AppHandle, Manager};
#[cfg(unix)]
use signal_hook::consts::SIGUSR2;
#[cfg(unix)]
use signal_hook::iterator::Signals;
#[cfg(unix)]
pub fn setup_signal_handler(app_handle: AppHandle, mut signals: Signals) {
let app_handle_for_signal = app_handle.clone();
debug!("SIGUSR2 signal handler registered successfully");
thread::spawn(move || {
debug!("SIGUSR2 signal handler thread started");
for sig in signals.forever() {
match sig {
SIGUSR2 => {
debug!("Received SIGUSR2 signal (signal number: {sig})");
let binding_id = "transcribe";
let shortcut_string = "SIGUSR2";
if let Some(action) = ACTION_MAP.get(binding_id) {
let toggle_state_manager =
app_handle_for_signal.state::<ManagedToggleState>();
let mut states = match toggle_state_manager.lock() {
Ok(s) => s,
Err(e) => {
warn!("Failed to lock toggle state manager: {e}");
continue;
}
};
let is_currently_active = states
.active_toggles
.entry(binding_id.to_string())
.or_insert(false);
if *is_currently_active {
debug!("SIGUSR2: Stopping transcription (currently active)");
action.stop(&app_handle_for_signal, binding_id, shortcut_string);
*is_currently_active = false; // Update state to inactive
debug!("SIGUSR2: Transcription stopped");
} else {
debug!("SIGUSR2: Starting transcription (currently inactive)");
action.start(&app_handle_for_signal, binding_id, shortcut_string);
*is_currently_active = true; // Update state to active
info!("SIGUSR2: Transcription started");
}
} else {
warn!("No action defined in ACTION_MAP for binding ID '{binding_id}'");
}
}
_ => unreachable!(),
}
}
});
}