diff --git a/src/config.rs b/src/config.rs index 98cba33..52758d0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,6 +5,7 @@ use std::env; use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; +use std::time::{Duration, Instant}; use crate::constants::{ CONFIG_BACKUP_NAME, CONFIG_DIR_NAME, CONFIG_FILE_NAME, SUPPORTED_KEYS, @@ -66,6 +67,8 @@ pub(crate) struct Config { pub(crate) reverse: bool, /// Suppress configured key events from reaching other apps. pub(crate) suppress: bool, + /// Delay deactivating the mic after releasing the configured keys. + pub(crate) release_delay_ms: u64, } /// Config data persisted to disk. @@ -84,6 +87,7 @@ pub(crate) struct PersistedConfig { pub(crate) startup_state: String, pub(crate) reverse: bool, pub(crate) suppress: bool, + pub(crate) release_delay_ms: u64, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -108,6 +112,7 @@ impl Default for PersistedConfig { startup_state: "muted".to_string(), reverse: false, suppress: false, + release_delay_ms: 0, } } } @@ -225,6 +230,16 @@ fn mode_label(mode: Mode) -> &'static str { } } +fn validate_release_delay(release_delay_ms: u64) -> Result<()> { + if Instant::now() + .checked_add(Duration::from_millis(release_delay_ms)) + .is_none() + { + bail!("--release-delay-ms is too large"); + } + Ok(()) +} + fn parse_mode(value: &str) -> Result { match value { "volume" => Ok(Mode::Volume), @@ -265,6 +280,7 @@ pub(crate) fn persisted_from_config(config: &Config) -> PersistedConfig { startup_state: startup_state_label(config.startup_state).to_string(), reverse: config.reverse, suppress: config.suppress, + release_delay_ms: config.release_delay_ms, } } @@ -299,6 +315,7 @@ pub(crate) fn print_persisted_config(path: &Path, config: &PersistedConfig) { println!("config_sound_volume: {}", config.sound_volume); println!("config_startup_state: {}", config.startup_state); println!("config_suppress: {}", config.suppress); + println!("config_release_delay_ms: {}", config.release_delay_ms); } fn sound_setting_value(setting: &SoundChoice) -> Option { @@ -373,6 +390,7 @@ Options:\n\ --no-reverse disable reverse behavior\n\ --on-level volume level when pressed (default: 1.0)\n\ --off-level volume level when released (default: 0.0)\n\ + --release-delay-ms delay deactivation after release (default: 0)\n\ --sound-on custom sound file for mic on (mp3/wav/ogg)\n\ --sound-off custom sound file for mic off (mp3/wav/ogg)\n\ --sound-volume sound volume (default: 1.0)\n\ @@ -441,9 +459,11 @@ pub(crate) fn print_config(config: &Config) { println!("sound_volume: {}", config.sound_volume); println!("startup_state: {startup_state}"); println!("suppress: {}", config.suppress); + println!("release_delay_ms: {}", config.release_delay_ms); } pub(crate) fn config_from_persisted(base: PersistedConfig) -> Result { + validate_release_delay(base.release_delay_ms)?; let mut keys: Vec = base .keys .iter() @@ -493,6 +513,7 @@ pub(crate) fn config_from_persisted(base: PersistedConfig) -> Result { dry_run: false, startup_state, suppress, + release_delay_ms: base.release_delay_ms, }) } @@ -521,6 +542,7 @@ pub(crate) fn parse_args(base: PersistedConfig) -> Result<(Config, bool)> { let mut startup_state = parse_startup_state(&base.startup_state)?; let mut startup_state_set = false; let mut suppress = base.suppress; + let mut release_delay_ms = base.release_delay_ms; let mut persist_changed = false; let mut key_set = false; @@ -622,6 +644,14 @@ pub(crate) fn parse_args(base: PersistedConfig) -> Result<(Config, bool)> { sounds = false; persist_changed = true; } + "--release-delay-ms" => { + i += 1; + let value = args.get(i).context("missing value for --release-delay-ms")?; + release_delay_ms = value + .parse::() + .with_context(|| format!("invalid --release-delay-ms '{value}'"))?; + persist_changed = true; + } "--suppress" => { suppress = true; persist_changed = true; @@ -661,6 +691,7 @@ pub(crate) fn parse_args(base: PersistedConfig) -> Result<(Config, bool)> { if reverse && !startup_state_set { startup_state = StartupState::Unmuted; } + validate_release_delay(release_delay_ms)?; Ok(( Config { @@ -680,6 +711,7 @@ pub(crate) fn parse_args(base: PersistedConfig) -> Result<(Config, bool)> { dry_run, startup_state, suppress, + release_delay_ms, }, persist_changed, )) diff --git a/src/main.rs b/src/main.rs index 83facbc..4f6cc3b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,7 @@ use std::os::unix::io::AsRawFd; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc; use std::sync::Arc; -use std::time::Duration; +use std::time::{Duration, Instant}; use crate::audio::{apply_off, apply_on, init_audio_cache, play_transition_sound}; use crate::config::{ @@ -107,10 +107,39 @@ fn update_pressed_keys(pressed: &mut HashSet, key: KeyCode, value: i32) } } +fn update_pending_release( + is_active: bool, + is_desired_on: bool, + is_reverse: bool, + release_delay_ms: u64, + pending_release: &mut Option, + now: Instant, +) -> Result<()> { + if is_desired_on || !is_active || is_reverse || release_delay_ms == 0 { + *pending_release = None; + } else if pending_release.is_none() { + *pending_release = Some( + now.checked_add(Duration::from_millis(release_delay_ms)) + .context("--release-delay-ms is too large")?, + ); + } + Ok(()) +} + +fn is_pending_release_expired(pending_release: &mut Option, now: Instant) -> bool { + if pending_release.is_some_and(|deadline| now >= deadline) { + *pending_release = None; + true + } else { + false + } +} + fn refresh_active_state( config: &Config, pressed: &HashSet, active: &mut bool, + pending_release: &mut Option, ) -> Result<()> { let all_pressed = config.keys.iter().all(|k| pressed.contains(k)); let desired_on = if config.reverse { @@ -118,8 +147,18 @@ fn refresh_active_state( } else { all_pressed }; - if desired_on != *active { - set_active_state(config, active, desired_on)?; + update_pending_release( + *active, + desired_on, + config.reverse, + config.release_delay_ms, + pending_release, + Instant::now(), + )?; + if desired_on && !*active { + set_active_state(config, active, true)?; + } else if !desired_on && *active && (config.reverse || config.release_delay_ms == 0) { + set_active_state(config, active, false)?; } Ok(()) } @@ -129,6 +168,7 @@ fn handle_events( device: &mut Device, pressed: &mut HashSet, active: &mut bool, + pending_release: &mut Option, virtual_device: &mut Option, ) -> Result> { let fetch_error = match device.fetch_events() { @@ -138,7 +178,7 @@ fn handle_events( let summary = ev.destructure(); if let EventSummary::Key(_, key, value) = summary { update_pressed_keys(pressed, key, value); - refresh_active_state(config, pressed, active)?; + refresh_active_state(config, pressed, active, pending_release)?; } if let Some(virtual_device) = virtual_device.as_mut() { match summary { @@ -397,8 +437,9 @@ fn main() -> Result<()> { let mut pressed: HashSet = HashSet::new(); let mut active = false; + let mut pending_release = None; - refresh_active_state(&config, &pressed, &mut active)?; + refresh_active_state(&config, &pressed, &mut active, &mut pending_release)?; while running.load(Ordering::SeqCst) { if let Some(err) = handle_events( @@ -406,11 +447,13 @@ fn main() -> Result<()> { &mut device, &mut pressed, &mut active, + &mut pending_release, &mut virtual_device, )? { eprintln!("Input device error: {err}. Reopening..."); apply_off(&config)?; active = false; + pending_release = None; pressed.clear(); device = reopen_device_loop(&config)?; virtual_device = apply_device_suppression(&config, &mut device)?; @@ -421,6 +464,7 @@ fn main() -> Result<()> { let device_changed = config.device_path != new_config.device_path; let suppress_changed = config.suppress != new_config.suppress; config = new_config; + pending_release = None; if let Err(err) = init_audio_cache(&config) { eprintln!("Failed to reload sounds: {err}"); } @@ -434,10 +478,14 @@ fn main() -> Result<()> { if suppress_changed && !(keys_changed || device_changed) { virtual_device = apply_device_suppression(&config, &mut device)?; } - refresh_active_state(&config, &pressed, &mut active)?; + refresh_active_state(&config, &pressed, &mut active, &mut pending_release)?; println!("Config reloaded"); } + if is_pending_release_expired(&mut pending_release, Instant::now()) { + set_active_state(&config, &mut active, false)?; + } + let sleep_ms = if config.suppress { 1 } else { 10 }; std::thread::sleep(Duration::from_millis(sleep_ms)); }