1#![windows_subsystem = "windows"]
2
3use std::collections::HashSet;
4use std::{collections::HashMap, thread, time::Duration};
5
6use device_query::{DeviceState, Keycode};
7use quad_snd::{AudioContext, Sound};
8use trayicon::{MenuBuilder, TrayIconBuilder};
9
10#[derive(PartialEq, Clone)]
11enum TrayEvents {
12 ShowMenu,
13 ToggleSound,
14 Quit,
15}
16
17fn main() {
18 let on_icon = trayicon::Icon::from_buffer(
19 Box::new(std::fs::read("osuclack.ico").unwrap()).leak(),
20 None,
21 None,
22 )
23 .unwrap();
24 let off_icon = trayicon::Icon::from_buffer(
25 Box::new(std::fs::read("osuclack_mute.ico").unwrap()).leak(),
26 None,
27 None,
28 )
29 .unwrap();
30 let (tray_tx, tray_rx) = std::sync::mpsc::channel();
31 let mut tray_icon = TrayIconBuilder::new()
32 .tooltip("osuclack")
33 .icon(on_icon.clone())
34 .on_click(TrayEvents::ToggleSound)
35 .on_right_click(TrayEvents::ShowMenu)
36 .menu(MenuBuilder::new().item("quit", TrayEvents::Quit))
37 .sender({
38 let tray_tx = tray_tx.clone();
39 move |e| tray_tx.send(e.clone()).unwrap()
40 })
41 .build()
42 .unwrap();
43
44 thread::spawn(move || {
45 let ctx = AudioContext::new();
46 let sounds = std::fs::read_dir("sounds")
47 .expect("cant read sounds")
48 .flat_map(|f| {
49 let p = f.ok()?.path();
50 let n = p.file_stem()?.to_string_lossy().into_owned();
51 (n != "LICENSE" && n != "README").then(|| {
52 (
53 n,
54 Sound::load(&ctx, &std::fs::read(p).expect("can't load sound")),
55 )
56 })
57 })
58 .collect::<HashMap<String, Sound>>();
59 let play_sound = |name: &str| {
60 let sound = sounds.get(name).unwrap();
61 sound.play(&ctx, Default::default());
62 };
63 let play_sound_for_key = |key: Keycode| match key {
64 Keycode::CapsLock => play_sound("caps"),
65 Keycode::Delete | Keycode::Backspace => play_sound("delete"),
66 Keycode::Up | Keycode::Down | Keycode::Left | Keycode::Right => play_sound("movement"),
67 _ => {
68 let no = fastrand::u8(1..=4);
69 play_sound(&format!("press-{no}"));
70 }
71 };
72
73 let state = DeviceState::new();
74 let mut previously_held_keys = HashSet::<Keycode>::new();
75 let mut key_press_times = HashMap::<Keycode, std::time::Instant>::new();
76 let mut last_sound_time = std::time::Instant::now();
77 let mut sound_enabled = true;
78
79 let initial_delay = Duration::from_millis(500); // Wait 500ms before starting to repeat
80 let repeat_interval = Duration::from_millis(50); // Then repeat every 50ms
81
82 loop {
83 let currently_held_keys: HashSet<Keycode> = state.query_keymap().into_iter().collect();
84
85 // Check for toggle hotkey (Ctrl + Alt + L/R Shift + C)
86 let hotkey_combo = [
87 [Keycode::LControl, Keycode::RControl], // Either left or right control
88 [Keycode::LAlt, Keycode::RAlt], // Either left or right alt
89 [Keycode::LShift, Keycode::RShift], // Either left or right shift
90 [Keycode::C, Keycode::C], // C key (duplicated for array consistency)
91 ];
92
93 let check_hotkey = |current: &HashSet<Keycode>, previous: &HashSet<Keycode>| {
94 hotkey_combo.iter().all(|key_group| {
95 key_group
96 .iter()
97 .any(|key| current.contains(key) || previous.contains(key))
98 })
99 };
100
101 let hotkey_active = check_hotkey(¤tly_held_keys, &previously_held_keys);
102 let hotkey_was_active = check_hotkey(&previously_held_keys, &HashSet::new());
103
104 if hotkey_active && !hotkey_was_active {
105 tray_tx.send(TrayEvents::ToggleSound).unwrap();
106 }
107
108 // handle tray events
109 if let Ok(event) = tray_rx.try_recv() {
110 match event {
111 TrayEvents::ToggleSound => {
112 sound_enabled = !sound_enabled;
113 tray_icon
114 .set_icon(sound_enabled.then_some(&on_icon).unwrap_or(&off_icon))
115 .unwrap();
116 }
117 TrayEvents::Quit => {
118 std::process::exit(0);
119 }
120 TrayEvents::ShowMenu => {
121 tray_icon.show_menu().unwrap();
122 }
123 }
124 }
125
126 // Only process sound logic if sounds are enabled
127 if sound_enabled {
128 // Track when keys were first pressed
129 for key in ¤tly_held_keys {
130 if !previously_held_keys.contains(key) {
131 // Key just pressed, record the time and play initial sound
132 key_press_times.insert(*key, std::time::Instant::now());
133 play_sound_for_key(*key);
134 }
135 }
136
137 // Remove timing info for released keys
138 key_press_times.retain(|key, _| currently_held_keys.contains(key));
139
140 // Play repeating sounds every 50ms, but only after initial delay
141 if last_sound_time.elapsed() >= repeat_interval {
142 let now = std::time::Instant::now();
143 for key in ¤tly_held_keys {
144 if is_modifier_key(key) {
145 continue;
146 }
147 if let Some(press_time) = key_press_times.get(key) {
148 // Only repeat if key has been held longer than initial delay
149 if now.duration_since(*press_time) >= initial_delay {
150 play_sound_for_key(*key);
151 }
152 }
153 }
154 last_sound_time = now;
155 }
156 } else {
157 // Clear key press times when sounds are disabled to avoid stale data
158 key_press_times.clear();
159 }
160
161 previously_held_keys = currently_held_keys;
162
163 // Buffer inputs at lower interval (5ms)
164 thread::sleep(Duration::from_millis(5));
165 }
166 });
167
168 loop {
169 use std::mem;
170 use winapi::um::winuser;
171
172 unsafe {
173 let mut msg = mem::MaybeUninit::uninit();
174 let bret = winuser::GetMessageA(msg.as_mut_ptr(), 0 as _, 0, 0);
175 if bret > 0 {
176 winuser::TranslateMessage(msg.as_ptr());
177 winuser::DispatchMessageA(msg.as_ptr());
178 } else {
179 break;
180 }
181 }
182 }
183}
184
185fn is_modifier_key(key: &Keycode) -> bool {
186 matches!(
187 key,
188 Keycode::LShift
189 | Keycode::RShift
190 | Keycode::LControl
191 | Keycode::RControl
192 | Keycode::RAlt
193 | Keycode::LAlt
194 | Keycode::RMeta
195 | Keycode::LMeta
196 | Keycode::CapsLock
197 )
198}