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
32 changes: 32 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -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)]
Expand All @@ -108,6 +112,7 @@ impl Default for PersistedConfig {
startup_state: "muted".to_string(),
reverse: false,
suppress: false,
release_delay_ms: 0,
}
}
}
Expand Down Expand Up @@ -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<Mode> {
match value {
"volume" => Ok(Mode::Volume),
Expand Down Expand Up @@ -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,
}
}

Expand Down Expand Up @@ -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<SoundSettingValue> {
Expand Down Expand Up @@ -373,6 +390,7 @@ Options:\n\
--no-reverse disable reverse behavior\n\
--on-level <FLOAT> volume level when pressed (default: 1.0)\n\
--off-level <FLOAT> volume level when released (default: 0.0)\n\
--release-delay-ms <MILLISECONDS> delay deactivation after release (default: 0)\n\
--sound-on <PATH> custom sound file for mic on (mp3/wav/ogg)\n\
--sound-off <PATH> custom sound file for mic off (mp3/wav/ogg)\n\
--sound-volume <FLOAT> sound volume (default: 1.0)\n\
Expand Down Expand Up @@ -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<Config> {
validate_release_delay(base.release_delay_ms)?;
let mut keys: Vec<KeyCode> = base
.keys
.iter()
Expand Down Expand Up @@ -493,6 +513,7 @@ pub(crate) fn config_from_persisted(base: PersistedConfig) -> Result<Config> {
dry_run: false,
startup_state,
suppress,
release_delay_ms: base.release_delay_ms,
})
}

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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::<u64>()
.with_context(|| format!("invalid --release-delay-ms '{value}'"))?;
persist_changed = true;
}
"--suppress" => {
suppress = true;
persist_changed = true;
Expand Down Expand Up @@ -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 {
Expand All @@ -680,6 +711,7 @@ pub(crate) fn parse_args(base: PersistedConfig) -> Result<(Config, bool)> {
dry_run,
startup_state,
suppress,
release_delay_ms,
},
persist_changed,
))
Expand Down
60 changes: 54 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -107,19 +107,58 @@ fn update_pressed_keys(pressed: &mut HashSet<KeyCode>, 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<Instant>,
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<Instant>, 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<KeyCode>,
active: &mut bool,
pending_release: &mut Option<Instant>,
) -> Result<()> {
let all_pressed = config.keys.iter().all(|k| pressed.contains(k));
let desired_on = if config.reverse {
!all_pressed
} 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(())
}
Expand All @@ -129,6 +168,7 @@ fn handle_events(
device: &mut Device,
pressed: &mut HashSet<KeyCode>,
active: &mut bool,
pending_release: &mut Option<Instant>,
virtual_device: &mut Option<VirtualDevice>,
) -> Result<Option<std::io::Error>> {
let fetch_error = match device.fetch_events() {
Expand All @@ -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 {
Expand Down Expand Up @@ -397,20 +437,23 @@ fn main() -> Result<()> {

let mut pressed: HashSet<KeyCode> = 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(
&config,
&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)?;
Expand All @@ -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}");
}
Expand All @@ -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));
}
Expand Down