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