when osu sound i guess

Compare changes

Choose any two refs to compare.

+1
.gitignore
··· 1 1 /target 2 + osuclack
+241
Cargo.lock
··· 21 21 ] 22 22 23 23 [[package]] 24 + name = "bitflags" 25 + version = "2.9.4" 26 + source = "registry+https://github.com/rust-lang/crates.io-index" 27 + checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" 28 + 29 + [[package]] 30 + name = "bitvec" 31 + version = "1.0.1" 32 + source = "registry+https://github.com/rust-lang/crates.io-index" 33 + checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" 34 + dependencies = [ 35 + "funty", 36 + "radium", 37 + "tap", 38 + "wyz", 39 + ] 40 + 41 + [[package]] 42 + name = "block2" 43 + version = "0.6.1" 44 + source = "registry+https://github.com/rust-lang/crates.io-index" 45 + checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" 46 + dependencies = [ 47 + "objc2", 48 + ] 49 + 50 + [[package]] 24 51 name = "byteorder" 25 52 version = "1.5.0" 26 53 source = "registry+https://github.com/rust-lang/crates.io-index" 27 54 checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 28 55 29 56 [[package]] 57 + name = "cfg-if" 58 + version = "1.0.3" 59 + source = "registry+https://github.com/rust-lang/crates.io-index" 60 + checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" 61 + 62 + [[package]] 63 + name = "cfg_aliases" 64 + version = "0.2.1" 65 + source = "registry+https://github.com/rust-lang/crates.io-index" 66 + checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 67 + 68 + [[package]] 30 69 name = "core-foundation" 31 70 version = "0.9.4" 32 71 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 69 108 "readmouse", 70 109 "windows", 71 110 "x11", 111 + ] 112 + 113 + [[package]] 114 + name = "dispatch2" 115 + version = "0.3.0" 116 + source = "registry+https://github.com/rust-lang/crates.io-index" 117 + checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" 118 + dependencies = [ 119 + "bitflags", 120 + "objc2", 121 + ] 122 + 123 + [[package]] 124 + name = "evdev" 125 + version = "0.13.2" 126 + source = "registry+https://github.com/rust-lang/crates.io-index" 127 + checksum = "25b686663ba7f08d92880ff6ba22170f1df4e83629341cba34cf82cd65ebea99" 128 + dependencies = [ 129 + "bitvec", 130 + "cfg-if", 131 + "libc", 132 + "nix", 72 133 ] 73 134 74 135 [[package]] ··· 78 139 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 79 140 80 141 [[package]] 142 + name = "funty" 143 + version = "2.0.0" 144 + source = "registry+https://github.com/rust-lang/crates.io-index" 145 + checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" 146 + 147 + [[package]] 81 148 name = "hound" 82 149 version = "3.5.1" 83 150 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 117 184 checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" 118 185 119 186 [[package]] 187 + name = "nix" 188 + version = "0.29.0" 189 + source = "registry+https://github.com/rust-lang/crates.io-index" 190 + checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 191 + dependencies = [ 192 + "bitflags", 193 + "cfg-if", 194 + "cfg_aliases", 195 + "libc", 196 + ] 197 + 198 + [[package]] 199 + name = "objc2" 200 + version = "0.6.2" 201 + source = "registry+https://github.com/rust-lang/crates.io-index" 202 + checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" 203 + dependencies = [ 204 + "objc2-encode", 205 + ] 206 + 207 + [[package]] 208 + name = "objc2-app-kit" 209 + version = "0.3.1" 210 + source = "registry+https://github.com/rust-lang/crates.io-index" 211 + checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" 212 + dependencies = [ 213 + "bitflags", 214 + "block2", 215 + "libc", 216 + "objc2", 217 + "objc2-cloud-kit", 218 + "objc2-core-data", 219 + "objc2-core-foundation", 220 + "objc2-core-graphics", 221 + "objc2-core-image", 222 + "objc2-foundation", 223 + "objc2-quartz-core", 224 + ] 225 + 226 + [[package]] 227 + name = "objc2-cloud-kit" 228 + version = "0.3.1" 229 + source = "registry+https://github.com/rust-lang/crates.io-index" 230 + checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d" 231 + dependencies = [ 232 + "bitflags", 233 + "objc2", 234 + "objc2-foundation", 235 + ] 236 + 237 + [[package]] 238 + name = "objc2-core-data" 239 + version = "0.3.1" 240 + source = "registry+https://github.com/rust-lang/crates.io-index" 241 + checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d" 242 + dependencies = [ 243 + "bitflags", 244 + "objc2", 245 + "objc2-foundation", 246 + ] 247 + 248 + [[package]] 249 + name = "objc2-core-foundation" 250 + version = "0.3.1" 251 + source = "registry+https://github.com/rust-lang/crates.io-index" 252 + checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" 253 + dependencies = [ 254 + "bitflags", 255 + "dispatch2", 256 + "objc2", 257 + ] 258 + 259 + [[package]] 260 + name = "objc2-core-graphics" 261 + version = "0.3.1" 262 + source = "registry+https://github.com/rust-lang/crates.io-index" 263 + checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" 264 + dependencies = [ 265 + "bitflags", 266 + "dispatch2", 267 + "objc2", 268 + "objc2-core-foundation", 269 + "objc2-io-surface", 270 + ] 271 + 272 + [[package]] 273 + name = "objc2-core-image" 274 + version = "0.3.1" 275 + source = "registry+https://github.com/rust-lang/crates.io-index" 276 + checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e" 277 + dependencies = [ 278 + "objc2", 279 + "objc2-foundation", 280 + ] 281 + 282 + [[package]] 283 + name = "objc2-encode" 284 + version = "4.1.0" 285 + source = "registry+https://github.com/rust-lang/crates.io-index" 286 + checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" 287 + 288 + [[package]] 289 + name = "objc2-foundation" 290 + version = "0.3.1" 291 + source = "registry+https://github.com/rust-lang/crates.io-index" 292 + checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" 293 + dependencies = [ 294 + "bitflags", 295 + "block2", 296 + "libc", 297 + "objc2", 298 + "objc2-core-foundation", 299 + ] 300 + 301 + [[package]] 302 + name = "objc2-io-surface" 303 + version = "0.3.1" 304 + source = "registry+https://github.com/rust-lang/crates.io-index" 305 + checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" 306 + dependencies = [ 307 + "bitflags", 308 + "objc2", 309 + "objc2-core-foundation", 310 + ] 311 + 312 + [[package]] 313 + name = "objc2-quartz-core" 314 + version = "0.3.1" 315 + source = "registry+https://github.com/rust-lang/crates.io-index" 316 + checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5" 317 + dependencies = [ 318 + "bitflags", 319 + "objc2", 320 + "objc2-foundation", 321 + ] 322 + 323 + [[package]] 120 324 name = "ogg" 121 325 version = "0.7.1" 122 326 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 130 334 version = "0.1.0" 131 335 dependencies = [ 132 336 "device_query", 337 + "evdev", 133 338 "fastrand", 339 + "libc", 134 340 "quad-snd", 341 + "trayicon", 342 + "winapi", 135 343 ] 136 344 137 345 [[package]] ··· 163 371 ] 164 372 165 373 [[package]] 374 + name = "radium" 375 + version = "0.7.0" 376 + source = "registry+https://github.com/rust-lang/crates.io-index" 377 + checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" 378 + 379 + [[package]] 166 380 name = "readkey" 167 381 version = "0.2.2" 168 382 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 181 395 checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" 182 396 dependencies = [ 183 397 "maybe-uninit", 398 + ] 399 + 400 + [[package]] 401 + name = "tap" 402 + version = "1.0.1" 403 + source = "registry+https://github.com/rust-lang/crates.io-index" 404 + checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" 405 + 406 + [[package]] 407 + name = "trayicon" 408 + version = "0.3.0" 409 + source = "registry+https://github.com/rust-lang/crates.io-index" 410 + checksum = "09786cfec3e032ab0536dfe8a84b015661e89c74b0c9278ad67c65a55270e0e2" 411 + dependencies = [ 412 + "objc2", 413 + "objc2-app-kit", 414 + "objc2-foundation", 415 + "winapi", 184 416 ] 185 417 186 418 [[package]] ··· 270 502 version = "0.48.5" 271 503 source = "registry+https://github.com/rust-lang/crates.io-index" 272 504 checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 505 + 506 + [[package]] 507 + name = "wyz" 508 + version = "0.5.1" 509 + source = "registry+https://github.com/rust-lang/crates.io-index" 510 + checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" 511 + dependencies = [ 512 + "tap", 513 + ] 273 514 274 515 [[package]] 275 516 name = "x11"
+20 -1
Cargo.toml
··· 3 3 version = "0.1.0" 4 4 edition = "2024" 5 5 6 + [features] 7 + tray = ["dep:trayicon", "dep:winapi"] 8 + 6 9 [dependencies] 7 - device_query = "4.0.1" 8 10 fastrand = "2.3.0" 9 11 quad-snd = "0.2.8" 12 + trayicon = { version = "0.3.0", optional = true } 13 + winapi = { version = "0.3.9", features = [ 14 + "winuser", 15 + "windef", 16 + "minwindef", 17 + "shellapi", 18 + "libloaderapi", 19 + "commctrl", 20 + "basetsd", 21 + ], optional = true } 22 + 23 + [target.'cfg(target_os = "linux")'.dependencies] 24 + evdev = "0.13.2" 25 + libc = "0.2" 26 + 27 + [target.'cfg(not(target_os = "linux"))'.dependencies] 28 + device_query = "4.0.1"
osuclack.ico

This is a binary file and will not be displayed.

osuclack_mute.ico

This is a binary file and will not be displayed.

+254
src/input.rs
··· 1 + #[cfg(target_os = "linux")] 2 + mod linux { 3 + use evdev::{Device, EventSummary, KeyCode}; 4 + use std::collections::HashSet; 5 + use std::fs::{self, OpenOptions}; 6 + use std::os::unix::io::AsRawFd; 7 + use std::path::Path; 8 + 9 + pub struct EvdevInput { 10 + devices: Vec<Device>, 11 + held_keys: HashSet<u16>, 12 + } 13 + 14 + impl EvdevInput { 15 + pub fn new() -> std::io::Result<Self> { 16 + let mut devices = Vec::new(); 17 + 18 + // Find all keyboard devices in /dev/input 19 + let input_dir = Path::new("/dev/input"); 20 + if let Ok(entries) = fs::read_dir(input_dir) { 21 + for entry in entries.flatten() { 22 + let path = entry.path(); 23 + if let Some(name) = path.file_name() { 24 + if name.to_string_lossy().starts_with("event") { 25 + if let Ok(file) = OpenOptions::new().read(true).open(&path) { 26 + // Set non-blocking mode 27 + let fd = file.as_raw_fd(); 28 + unsafe { 29 + let flags = libc::fcntl(fd, libc::F_GETFL, 0); 30 + if flags != -1 { 31 + libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK); 32 + } 33 + } 34 + 35 + if let Ok(device) = Device::from_fd(file.into()) { 36 + // Check if this device supports keyboard events 37 + if device.supported_keys().map_or(false, |keys| { 38 + keys.contains(KeyCode::KEY_A) || keys.contains(KeyCode::KEY_SPACE) 39 + }) { 40 + devices.push(device); 41 + } 42 + } 43 + } 44 + } 45 + } 46 + } 47 + } 48 + 49 + if devices.is_empty() { 50 + eprintln!("Warning: No keyboard devices found. Make sure you have read permissions for /dev/input/event* devices."); 51 + eprintln!("You may need to add your user to the 'input' group or run with appropriate permissions."); 52 + } 53 + 54 + Ok(Self { 55 + devices, 56 + held_keys: HashSet::new(), 57 + }) 58 + } 59 + 60 + pub fn query_keymap(&mut self) -> HashSet<u16> { 61 + // Process events from all devices 62 + for device in &mut self.devices { 63 + while let Ok(events) = device.fetch_events() { 64 + for event in events { 65 + if let EventSummary::Key(_, key, value) = event.destructure() { 66 + let code = key.code(); 67 + match value { 68 + 0 => { 69 + // Key released 70 + self.held_keys.remove(&code); 71 + } 72 + 1 | 2 => { 73 + // Key pressed (1) or repeated (2) 74 + self.held_keys.insert(code); 75 + } 76 + _ => {} 77 + } 78 + } 79 + } 80 + } 81 + } 82 + 83 + self.held_keys.clone() 84 + } 85 + } 86 + 87 + // Key code constants 88 + #[allow(dead_code)] 89 + pub const KEY_A: u16 = KeyCode::KEY_A.code(); 90 + pub const KEY_C: u16 = KeyCode::KEY_C.code(); 91 + pub const KEY_CAPSLOCK: u16 = KeyCode::KEY_CAPSLOCK.code(); 92 + pub const KEY_DELETE: u16 = KeyCode::KEY_DELETE.code(); 93 + pub const KEY_BACKSPACE: u16 = KeyCode::KEY_BACKSPACE.code(); 94 + pub const KEY_UP: u16 = KeyCode::KEY_UP.code(); 95 + pub const KEY_DOWN: u16 = KeyCode::KEY_DOWN.code(); 96 + pub const KEY_LEFT: u16 = KeyCode::KEY_LEFT.code(); 97 + pub const KEY_RIGHT: u16 = KeyCode::KEY_RIGHT.code(); 98 + pub const KEY_LEFTSHIFT: u16 = KeyCode::KEY_LEFTSHIFT.code(); 99 + pub const KEY_RIGHTSHIFT: u16 = KeyCode::KEY_RIGHTSHIFT.code(); 100 + pub const KEY_LEFTCTRL: u16 = KeyCode::KEY_LEFTCTRL.code(); 101 + pub const KEY_RIGHTCTRL: u16 = KeyCode::KEY_RIGHTCTRL.code(); 102 + pub const KEY_LEFTALT: u16 = KeyCode::KEY_LEFTALT.code(); 103 + pub const KEY_RIGHTALT: u16 = KeyCode::KEY_RIGHTALT.code(); 104 + pub const KEY_LEFTMETA: u16 = KeyCode::KEY_LEFTMETA.code(); 105 + pub const KEY_RIGHTMETA: u16 = KeyCode::KEY_RIGHTMETA.code(); 106 + 107 + pub fn is_modifier_key(key: u16) -> bool { 108 + matches!( 109 + key, 110 + KEY_LEFTSHIFT 111 + | KEY_RIGHTSHIFT 112 + | KEY_LEFTCTRL 113 + | KEY_RIGHTCTRL 114 + | KEY_RIGHTALT 115 + | KEY_LEFTALT 116 + | KEY_RIGHTMETA 117 + | KEY_LEFTMETA 118 + | KEY_CAPSLOCK 119 + ) 120 + } 121 + } 122 + 123 + #[cfg(not(target_os = "linux"))] 124 + mod fallback { 125 + use device_query::{DeviceQuery, DeviceState, Keycode}; 126 + use std::collections::HashSet; 127 + 128 + pub struct DeviceQueryInput { 129 + state: DeviceState, 130 + } 131 + 132 + impl DeviceQueryInput { 133 + pub fn new() -> Self { 134 + Self { 135 + state: DeviceState::new(), 136 + } 137 + } 138 + 139 + pub fn query_keymap(&mut self) -> HashSet<u16> { 140 + self.state 141 + .query_keymap() 142 + .into_iter() 143 + .map(keycode_to_u16) 144 + .collect() 145 + } 146 + } 147 + 148 + fn keycode_to_u16(keycode: Keycode) -> u16 { 149 + // Map device_query keycodes to arbitrary u16 values 150 + // We use values that won't collide with Linux key codes 151 + match keycode { 152 + Keycode::A => 1000, 153 + Keycode::B => 1001, 154 + Keycode::C => 1002, 155 + Keycode::D => 1003, 156 + Keycode::E => 1004, 157 + Keycode::F => 1005, 158 + Keycode::G => 1006, 159 + Keycode::H => 1007, 160 + Keycode::I => 1008, 161 + Keycode::J => 1009, 162 + Keycode::K => 1010, 163 + Keycode::L => 1011, 164 + Keycode::M => 1012, 165 + Keycode::N => 1013, 166 + Keycode::O => 1014, 167 + Keycode::P => 1015, 168 + Keycode::Q => 1016, 169 + Keycode::R => 1017, 170 + Keycode::S => 1018, 171 + Keycode::T => 1019, 172 + Keycode::U => 1020, 173 + Keycode::V => 1021, 174 + Keycode::W => 1022, 175 + Keycode::X => 1023, 176 + Keycode::Y => 1024, 177 + Keycode::Z => 1025, 178 + Keycode::CapsLock => 1026, 179 + Keycode::Delete => 1027, 180 + Keycode::Backspace => 1028, 181 + Keycode::Up => 1029, 182 + Keycode::Down => 1030, 183 + Keycode::Left => 1031, 184 + Keycode::Right => 1032, 185 + Keycode::LShift => 1033, 186 + Keycode::RShift => 1034, 187 + Keycode::LControl => 1035, 188 + Keycode::RControl => 1036, 189 + Keycode::LAlt => 1037, 190 + Keycode::RAlt => 1038, 191 + Keycode::LMeta => 1039, 192 + Keycode::RMeta => 1040, 193 + _ => { 194 + // For any other key, use a hash of the debug string 195 + use std::collections::hash_map::DefaultHasher; 196 + use std::hash::{Hash, Hasher}; 197 + let mut hasher = DefaultHasher::new(); 198 + format!("{:?}", keycode).hash(&mut hasher); 199 + (hasher.finish() as u16).wrapping_add(2000) 200 + } 201 + } 202 + } 203 + 204 + // Key code constants - matching the values in keycode_to_u16 205 + #[allow(dead_code)] 206 + pub const KEY_A: u16 = 1000; 207 + pub const KEY_C: u16 = 1002; 208 + pub const KEY_CAPSLOCK: u16 = 1026; 209 + pub const KEY_DELETE: u16 = 1027; 210 + pub const KEY_BACKSPACE: u16 = 1028; 211 + pub const KEY_UP: u16 = 1029; 212 + pub const KEY_DOWN: u16 = 1030; 213 + pub const KEY_LEFT: u16 = 1031; 214 + pub const KEY_RIGHT: u16 = 1032; 215 + pub const KEY_LEFTSHIFT: u16 = 1033; 216 + pub const KEY_RIGHTSHIFT: u16 = 1034; 217 + pub const KEY_LEFTCTRL: u16 = 1035; 218 + pub const KEY_RIGHTCTRL: u16 = 1036; 219 + pub const KEY_LEFTALT: u16 = 1037; 220 + pub const KEY_RIGHTALT: u16 = 1038; 221 + pub const KEY_LEFTMETA: u16 = 1039; 222 + pub const KEY_RIGHTMETA: u16 = 1040; 223 + 224 + pub fn is_modifier_key(key: u16) -> bool { 225 + matches!( 226 + key, 227 + KEY_LEFTSHIFT 228 + | KEY_RIGHTSHIFT 229 + | KEY_LEFTCTRL 230 + | KEY_RIGHTCTRL 231 + | KEY_RIGHTALT 232 + | KEY_LEFTALT 233 + | KEY_RIGHTMETA 234 + | KEY_LEFTMETA 235 + | KEY_CAPSLOCK 236 + ) 237 + } 238 + } 239 + 240 + #[cfg(target_os = "linux")] 241 + pub use linux::*; 242 + 243 + #[cfg(not(target_os = "linux"))] 244 + pub use fallback::*; 245 + 246 + #[cfg(target_os = "linux")] 247 + pub type Input = linux::EvdevInput; 248 + 249 + #[cfg(not(target_os = "linux"))] 250 + pub type Input = fallback::DeviceQueryInput; 251 + 252 + pub fn create_input() -> std::io::Result<Input> { 253 + Input::new() 254 + }
+184 -36
src/main.rs
··· 1 1 #![windows_subsystem = "windows"] 2 2 3 - use std::sync::mpsc; 4 - use std::{collections::HashMap, time::Duration}; 3 + use std::collections::HashSet; 4 + use std::{collections::HashMap, thread, time::Duration}; 5 5 6 - use device_query::{DeviceEvents, DeviceEventsHandler, Keycode}; 7 6 use quad_snd::{AudioContext, Sound}; 7 + #[cfg(feature = "tray")] 8 + use trayicon::{MenuBuilder, TrayIconBuilder}; 9 + 10 + mod input; 11 + use input::*; 12 + 13 + #[cfg(feature = "tray")] 14 + #[derive(PartialEq, Clone)] 15 + enum TrayEvents { 16 + ShowMenu, 17 + ToggleSound, 18 + Quit, 19 + } 8 20 9 21 fn main() { 10 - let ctx = AudioContext::new(); 11 - let sounds = std::fs::read_dir("sounds") 12 - .expect("cant read sounds") 13 - .flat_map(|f| { 14 - let p = f.ok()?.path(); 15 - let n = p.file_stem()?.to_string_lossy().into_owned(); 16 - (n != "LICENSE" && n != "README").then(|| { 17 - ( 18 - n, 19 - Sound::load(&ctx, &std::fs::read(p).expect("can't load sound")), 20 - ) 21 - }) 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() 22 48 }) 23 - .collect::<HashMap<String, Sound>>(); 24 - let play_sound = |name: &str| { 25 - let sound = sounds.get(name).unwrap(); 26 - sound.play(&ctx, Default::default()); 27 - }; 49 + .build() 50 + .unwrap(); 28 51 29 - let (tx, rx) = mpsc::channel(); 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 + }; 30 80 31 - let query = DeviceEventsHandler::new(Duration::from_millis(10)).expect("already running"); 32 - let _guard = query.on_key_down(move |key| { 33 - tx.send(*key).expect("tx"); 34 - }); 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; 35 86 36 - loop { 37 - if let Ok(key) = rx.recv() { 38 - match key { 39 - Keycode::CapsLock => play_sound("caps"), 40 - Keycode::Delete | Keycode::Backspace => play_sound("delete"), 41 - Keycode::Up | Keycode::Down | Keycode::Left | Keycode::Right => { 42 - play_sound("movement") 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(&currently_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 + } 43 137 } 44 - _ => { 45 - let no = fastrand::u8(1..=4); 46 - play_sound(&format!("press-{no}")); 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 &currently_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 + } 47 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 &currently_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(); 48 173 } 174 + 175 + previously_held_keys = currently_held_keys; 176 + 177 + // Buffer inputs at lower interval (5ms) 178 + thread::sleep(Duration::from_millis(5)); 49 179 } 50 - println!("handling new event"); 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 + } 51 197 } 198 + #[cfg(not(feature = "tray"))] 199 + _t.join().unwrap(); 52 200 }