#![windows_subsystem = "windows"] use std::collections::HashSet; use std::{collections::HashMap, thread, time::Duration}; use quad_snd::{AudioContext, Sound}; #[cfg(feature = "tray")] use trayicon::{MenuBuilder, TrayIconBuilder}; mod input; use input::*; #[cfg(feature = "tray")] #[derive(PartialEq, Clone)] enum TrayEvents { ShowMenu, ToggleSound, Quit, } fn main() { #[cfg(feature = "tray")] let on_icon = trayicon::Icon::from_buffer( Box::new(std::fs::read("osuclack.ico").unwrap()).leak(), None, None, ) .unwrap(); #[cfg(feature = "tray")] let off_icon = trayicon::Icon::from_buffer( Box::new(std::fs::read("osuclack_mute.ico").unwrap()).leak(), None, None, ) .unwrap(); #[cfg(feature = "tray")] let (tray_tx, tray_rx) = std::sync::mpsc::channel(); #[cfg(feature = "tray")] let mut tray_icon = TrayIconBuilder::new() .tooltip("osuclack") .icon(on_icon.clone()) .on_click(TrayEvents::ToggleSound) .on_right_click(TrayEvents::ShowMenu) .menu(MenuBuilder::new().item("quit", TrayEvents::Quit)) .sender({ let tray_tx = tray_tx.clone(); move |e| tray_tx.send(e.clone()).unwrap() }) .build() .unwrap(); let _t = thread::spawn(move || { let ctx = AudioContext::new(); let sounds = std::fs::read_dir("sounds") .expect("cant read sounds") .flat_map(|f| { let p = f.ok()?.path(); let n = p.file_stem()?.to_string_lossy().into_owned(); (n != "LICENSE" && n != "README").then(|| { ( n, Sound::load(&ctx, &std::fs::read(p).expect("can't load sound")), ) }) }) .collect::>(); let play_sound = |name: &str| { let sound = sounds.get(name).unwrap(); sound.play(&ctx, Default::default()); }; let play_sound_for_key = |key: u16| match key { KEY_CAPSLOCK => play_sound("caps"), KEY_DELETE | KEY_BACKSPACE => play_sound("delete"), KEY_UP | KEY_DOWN | KEY_LEFT | KEY_RIGHT => play_sound("movement"), _ => { let no = fastrand::u8(1..=4); play_sound(&format!("press-{no}")); } }; let mut input = create_input().expect("Failed to initialize input system"); let mut previously_held_keys = HashSet::::new(); let mut key_press_times = HashMap::::new(); let mut last_sound_time = std::time::Instant::now(); let mut sound_enabled = true; let initial_delay = Duration::from_millis(500); // Wait 500ms before starting to repeat let repeat_interval = Duration::from_millis(50); // Then repeat every 50ms loop { let currently_held_keys = input.query_keymap(); // Check for toggle hotkey (Ctrl + Alt + L/R Shift + C) let hotkey_combo = [ [KEY_LEFTCTRL, KEY_RIGHTCTRL], // Either left or right control [KEY_LEFTALT, KEY_RIGHTALT], // Either left or right alt [KEY_LEFTSHIFT, KEY_RIGHTSHIFT], // Either left or right shift [KEY_C, KEY_C], // C key (duplicated for array consistency) ]; let check_hotkey = |current: &HashSet, previous: &HashSet| { hotkey_combo.iter().all(|key_group| { key_group .iter() .any(|key| current.contains(key) || previous.contains(key)) }) }; let hotkey_active = check_hotkey(¤tly_held_keys, &previously_held_keys); let hotkey_was_active = check_hotkey(&previously_held_keys, &HashSet::new()); #[cfg(feature = "tray")] if hotkey_active && !hotkey_was_active { tray_tx.send(TrayEvents::ToggleSound).unwrap(); } if hotkey_active && !hotkey_was_active { sound_enabled = !sound_enabled; } // handle tray events #[cfg(feature = "tray")] if let Ok(event) = tray_rx.try_recv() { match event { TrayEvents::ToggleSound => { sound_enabled = !sound_enabled; tray_icon .set_icon(sound_enabled.then_some(&on_icon).unwrap_or(&off_icon)) .unwrap(); } TrayEvents::Quit => { std::process::exit(0); } TrayEvents::ShowMenu => { tray_icon.show_menu().unwrap(); } } } // Only process sound logic if sounds are enabled if sound_enabled { // Track when keys were first pressed for key in ¤tly_held_keys { if !previously_held_keys.contains(key) { // Key just pressed, record the time and play initial sound key_press_times.insert(*key, std::time::Instant::now()); play_sound_for_key(*key); } } // Remove timing info for released keys key_press_times.retain(|key, _| currently_held_keys.contains(key)); // Play repeating sounds every 50ms, but only after initial delay if last_sound_time.elapsed() >= repeat_interval { let now = std::time::Instant::now(); for key in ¤tly_held_keys { if is_modifier_key(*key) { continue; } if let Some(press_time) = key_press_times.get(key) { // Only repeat if key has been held longer than initial delay if now.duration_since(*press_time) >= initial_delay { play_sound_for_key(*key); } } } last_sound_time = now; } } else { // Clear key press times when sounds are disabled to avoid stale data key_press_times.clear(); } previously_held_keys = currently_held_keys; // Buffer inputs at lower interval (5ms) thread::sleep(Duration::from_millis(5)); } }); #[cfg(feature = "tray")] loop { use std::mem; use winapi::um::winuser; unsafe { let mut msg = mem::MaybeUninit::uninit(); let bret = winuser::GetMessageA(msg.as_mut_ptr(), 0 as _, 0, 0); if bret > 0 { winuser::TranslateMessage(msg.as_ptr()); winuser::DispatchMessageA(msg.as_ptr()); } else { break; } } } #[cfg(not(feature = "tray"))] _t.join().unwrap(); }