+241
Cargo.lock
+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
+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
osuclack.ico
This is a binary file and will not be displayed.
osuclack_mute.ico
osuclack_mute.ico
This is a binary file and will not be displayed.
+254
src/input.rs
+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
+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(¤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
+
}
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 ¤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
+
}
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 ¤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();
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
}