when osu sound i guess

feat: actually properly work on linux

ptr.pet 2224dff9 a5355bf0

verified
+1
.gitignore
···
/target
+
osuclack
+77
Cargo.lock
···
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
[[package]]
+
name = "bitvec"
+
version = "1.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
+
dependencies = [
+
"funty",
+
"radium",
+
"tap",
+
"wyz",
+
]
+
+
[[package]]
name = "block2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
+
name = "cfg-if"
+
version = "1.0.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
+
+
[[package]]
+
name = "cfg_aliases"
+
version = "0.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
]
[[package]]
+
name = "evdev"
+
version = "0.13.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "25b686663ba7f08d92880ff6ba22170f1df4e83629341cba34cf82cd65ebea99"
+
dependencies = [
+
"bitvec",
+
"cfg-if",
+
"libc",
+
"nix",
+
]
+
+
[[package]]
name = "fastrand"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
+
name = "funty"
+
version = "2.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
+
+
[[package]]
name = "hound"
version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"
+
+
[[package]]
+
name = "nix"
+
version = "0.29.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
+
dependencies = [
+
"bitflags",
+
"cfg-if",
+
"cfg_aliases",
+
"libc",
+
]
[[package]]
name = "objc2"
···
version = "0.1.0"
dependencies = [
"device_query",
+
"evdev",
"fastrand",
+
"libc",
"quad-snd",
"trayicon",
"winapi",
···
]
[[package]]
+
name = "radium"
+
version = "0.7.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
+
+
[[package]]
name = "readkey"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
···
dependencies = [
"maybe-uninit",
]
+
+
[[package]]
+
name = "tap"
+
version = "1.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "trayicon"
···
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+
[[package]]
+
name = "wyz"
+
version = "0.5.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
+
dependencies = [
+
"tap",
+
]
[[package]]
name = "x11"
+12 -3
Cargo.toml
···
version = "0.1.0"
edition = "2024"
+
[features]
+
tray = ["dep:trayicon", "dep:winapi"]
+
[dependencies]
-
device_query = "4.0.1"
fastrand = "2.3.0"
quad-snd = "0.2.8"
-
trayicon = "0.3.0"
+
trayicon = { version = "0.3.0", optional = true }
winapi = { version = "0.3.9", features = [
"winuser",
"windef",
···
"libloaderapi",
"commctrl",
"basetsd",
-
] }
+
], optional = true }
+
+
[target.'cfg(target_os = "linux")'.dependencies]
+
evdev = "0.13.2"
+
libc = "0.2"
+
+
[target.'cfg(not(target_os = "linux"))'.dependencies]
+
device_query = "4.0.1"
+254
src/input.rs
···
+
#[cfg(target_os = "linux")]
+
mod linux {
+
use evdev::{Device, EventSummary, KeyCode};
+
use std::collections::HashSet;
+
use std::fs::{self, OpenOptions};
+
use std::os::unix::io::AsRawFd;
+
use std::path::Path;
+
+
pub struct EvdevInput {
+
devices: Vec<Device>,
+
held_keys: HashSet<u16>,
+
}
+
+
impl EvdevInput {
+
pub fn new() -> std::io::Result<Self> {
+
let mut devices = Vec::new();
+
+
// Find all keyboard devices in /dev/input
+
let input_dir = Path::new("/dev/input");
+
if let Ok(entries) = fs::read_dir(input_dir) {
+
for entry in entries.flatten() {
+
let path = entry.path();
+
if let Some(name) = path.file_name() {
+
if name.to_string_lossy().starts_with("event") {
+
if let Ok(file) = OpenOptions::new().read(true).open(&path) {
+
// Set non-blocking mode
+
let fd = file.as_raw_fd();
+
unsafe {
+
let flags = libc::fcntl(fd, libc::F_GETFL, 0);
+
if flags != -1 {
+
libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK);
+
}
+
}
+
+
if let Ok(device) = Device::from_fd(file.into()) {
+
// Check if this device supports keyboard events
+
if device.supported_keys().map_or(false, |keys| {
+
keys.contains(KeyCode::KEY_A) || keys.contains(KeyCode::KEY_SPACE)
+
}) {
+
devices.push(device);
+
}
+
}
+
}
+
}
+
}
+
}
+
}
+
+
if devices.is_empty() {
+
eprintln!("Warning: No keyboard devices found. Make sure you have read permissions for /dev/input/event* devices.");
+
eprintln!("You may need to add your user to the 'input' group or run with appropriate permissions.");
+
}
+
+
Ok(Self {
+
devices,
+
held_keys: HashSet::new(),
+
})
+
}
+
+
pub fn query_keymap(&mut self) -> HashSet<u16> {
+
// Process events from all devices
+
for device in &mut self.devices {
+
while let Ok(events) = device.fetch_events() {
+
for event in events {
+
if let EventSummary::Key(_, key, value) = event.destructure() {
+
let code = key.code();
+
match value {
+
0 => {
+
// Key released
+
self.held_keys.remove(&code);
+
}
+
1 | 2 => {
+
// Key pressed (1) or repeated (2)
+
self.held_keys.insert(code);
+
}
+
_ => {}
+
}
+
}
+
}
+
}
+
}
+
+
self.held_keys.clone()
+
}
+
}
+
+
// Key code constants
+
#[allow(dead_code)]
+
pub const KEY_A: u16 = KeyCode::KEY_A.code();
+
pub const KEY_C: u16 = KeyCode::KEY_C.code();
+
pub const KEY_CAPSLOCK: u16 = KeyCode::KEY_CAPSLOCK.code();
+
pub const KEY_DELETE: u16 = KeyCode::KEY_DELETE.code();
+
pub const KEY_BACKSPACE: u16 = KeyCode::KEY_BACKSPACE.code();
+
pub const KEY_UP: u16 = KeyCode::KEY_UP.code();
+
pub const KEY_DOWN: u16 = KeyCode::KEY_DOWN.code();
+
pub const KEY_LEFT: u16 = KeyCode::KEY_LEFT.code();
+
pub const KEY_RIGHT: u16 = KeyCode::KEY_RIGHT.code();
+
pub const KEY_LEFTSHIFT: u16 = KeyCode::KEY_LEFTSHIFT.code();
+
pub const KEY_RIGHTSHIFT: u16 = KeyCode::KEY_RIGHTSHIFT.code();
+
pub const KEY_LEFTCTRL: u16 = KeyCode::KEY_LEFTCTRL.code();
+
pub const KEY_RIGHTCTRL: u16 = KeyCode::KEY_RIGHTCTRL.code();
+
pub const KEY_LEFTALT: u16 = KeyCode::KEY_LEFTALT.code();
+
pub const KEY_RIGHTALT: u16 = KeyCode::KEY_RIGHTALT.code();
+
pub const KEY_LEFTMETA: u16 = KeyCode::KEY_LEFTMETA.code();
+
pub const KEY_RIGHTMETA: u16 = KeyCode::KEY_RIGHTMETA.code();
+
+
pub fn is_modifier_key(key: u16) -> bool {
+
matches!(
+
key,
+
KEY_LEFTSHIFT
+
| KEY_RIGHTSHIFT
+
| KEY_LEFTCTRL
+
| KEY_RIGHTCTRL
+
| KEY_RIGHTALT
+
| KEY_LEFTALT
+
| KEY_RIGHTMETA
+
| KEY_LEFTMETA
+
| KEY_CAPSLOCK
+
)
+
}
+
}
+
+
#[cfg(not(target_os = "linux"))]
+
mod fallback {
+
use device_query::{DeviceQuery, DeviceState, Keycode};
+
use std::collections::HashSet;
+
+
pub struct DeviceQueryInput {
+
state: DeviceState,
+
}
+
+
impl DeviceQueryInput {
+
pub fn new() -> Self {
+
Self {
+
state: DeviceState::new(),
+
}
+
}
+
+
pub fn query_keymap(&mut self) -> HashSet<u16> {
+
self.state
+
.query_keymap()
+
.into_iter()
+
.map(keycode_to_u16)
+
.collect()
+
}
+
}
+
+
fn keycode_to_u16(keycode: Keycode) -> u16 {
+
// Map device_query keycodes to arbitrary u16 values
+
// We use values that won't collide with Linux key codes
+
match keycode {
+
Keycode::A => 1000,
+
Keycode::B => 1001,
+
Keycode::C => 1002,
+
Keycode::D => 1003,
+
Keycode::E => 1004,
+
Keycode::F => 1005,
+
Keycode::G => 1006,
+
Keycode::H => 1007,
+
Keycode::I => 1008,
+
Keycode::J => 1009,
+
Keycode::K => 1010,
+
Keycode::L => 1011,
+
Keycode::M => 1012,
+
Keycode::N => 1013,
+
Keycode::O => 1014,
+
Keycode::P => 1015,
+
Keycode::Q => 1016,
+
Keycode::R => 1017,
+
Keycode::S => 1018,
+
Keycode::T => 1019,
+
Keycode::U => 1020,
+
Keycode::V => 1021,
+
Keycode::W => 1022,
+
Keycode::X => 1023,
+
Keycode::Y => 1024,
+
Keycode::Z => 1025,
+
Keycode::CapsLock => 1026,
+
Keycode::Delete => 1027,
+
Keycode::Backspace => 1028,
+
Keycode::Up => 1029,
+
Keycode::Down => 1030,
+
Keycode::Left => 1031,
+
Keycode::Right => 1032,
+
Keycode::LShift => 1033,
+
Keycode::RShift => 1034,
+
Keycode::LControl => 1035,
+
Keycode::RControl => 1036,
+
Keycode::LAlt => 1037,
+
Keycode::RAlt => 1038,
+
Keycode::LMeta => 1039,
+
Keycode::RMeta => 1040,
+
_ => {
+
// For any other key, use a hash of the debug string
+
use std::collections::hash_map::DefaultHasher;
+
use std::hash::{Hash, Hasher};
+
let mut hasher = DefaultHasher::new();
+
format!("{:?}", keycode).hash(&mut hasher);
+
(hasher.finish() as u16).wrapping_add(2000)
+
}
+
}
+
}
+
+
// Key code constants - matching the values in keycode_to_u16
+
#[allow(dead_code)]
+
pub const KEY_A: u16 = 1000;
+
pub const KEY_C: u16 = 1002;
+
pub const KEY_CAPSLOCK: u16 = 1026;
+
pub const KEY_DELETE: u16 = 1027;
+
pub const KEY_BACKSPACE: u16 = 1028;
+
pub const KEY_UP: u16 = 1029;
+
pub const KEY_DOWN: u16 = 1030;
+
pub const KEY_LEFT: u16 = 1031;
+
pub const KEY_RIGHT: u16 = 1032;
+
pub const KEY_LEFTSHIFT: u16 = 1033;
+
pub const KEY_RIGHTSHIFT: u16 = 1034;
+
pub const KEY_LEFTCTRL: u16 = 1035;
+
pub const KEY_RIGHTCTRL: u16 = 1036;
+
pub const KEY_LEFTALT: u16 = 1037;
+
pub const KEY_RIGHTALT: u16 = 1038;
+
pub const KEY_LEFTMETA: u16 = 1039;
+
pub const KEY_RIGHTMETA: u16 = 1040;
+
+
pub fn is_modifier_key(key: u16) -> bool {
+
matches!(
+
key,
+
KEY_LEFTSHIFT
+
| KEY_RIGHTSHIFT
+
| KEY_LEFTCTRL
+
| KEY_RIGHTCTRL
+
| KEY_RIGHTALT
+
| KEY_LEFTALT
+
| KEY_RIGHTMETA
+
| KEY_LEFTMETA
+
| KEY_CAPSLOCK
+
)
+
}
+
}
+
+
#[cfg(target_os = "linux")]
+
pub use linux::*;
+
+
#[cfg(not(target_os = "linux"))]
+
pub use fallback::*;
+
+
#[cfg(target_os = "linux")]
+
pub type Input = linux::EvdevInput;
+
+
#[cfg(not(target_os = "linux"))]
+
pub type Input = fallback::DeviceQueryInput;
+
+
pub fn create_input() -> std::io::Result<Input> {
+
Input::new()
+
}
+33 -31
src/main.rs
···
use std::collections::HashSet;
use std::{collections::HashMap, thread, time::Duration};
-
use device_query::{DeviceState, Keycode};
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,
···
}
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())
···
.build()
.unwrap();
-
thread::spawn(move || {
+
let _t = thread::spawn(move || {
let ctx = AudioContext::new();
let sounds = std::fs::read_dir("sounds")
.expect("cant read sounds")
···
let sound = sounds.get(name).unwrap();
sound.play(&ctx, Default::default());
};
-
let play_sound_for_key = |key: Keycode| match key {
-
Keycode::CapsLock => play_sound("caps"),
-
Keycode::Delete | Keycode::Backspace => play_sound("delete"),
-
Keycode::Up | Keycode::Down | Keycode::Left | Keycode::Right => play_sound("movement"),
+
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 state = DeviceState::new();
-
let mut previously_held_keys = HashSet::<Keycode>::new();
-
let mut key_press_times = HashMap::<Keycode, std::time::Instant>::new();
+
let mut input = create_input().expect("Failed to initialize input system");
+
let mut previously_held_keys = HashSet::<u16>::new();
+
let mut key_press_times = HashMap::<u16, std::time::Instant>::new();
let mut last_sound_time = std::time::Instant::now();
let mut sound_enabled = true;
···
let repeat_interval = Duration::from_millis(50); // Then repeat every 50ms
loop {
-
let currently_held_keys: HashSet<Keycode> = state.query_keymap().into_iter().collect();
+
let currently_held_keys = input.query_keymap();
// Check for toggle hotkey (Ctrl + Alt + L/R Shift + C)
let hotkey_combo = [
-
[Keycode::LControl, Keycode::RControl], // Either left or right control
-
[Keycode::LAlt, Keycode::RAlt], // Either left or right alt
-
[Keycode::LShift, Keycode::RShift], // Either left or right shift
-
[Keycode::C, Keycode::C], // C key (duplicated for array consistency)
+
[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<Keycode>, previous: &HashSet<Keycode>| {
+
let check_hotkey = |current: &HashSet<u16>, previous: &HashSet<u16>| {
hotkey_combo.iter().all(|key_group| {
key_group
.iter()
···
let hotkey_active = check_hotkey(&currently_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 => {
···
if last_sound_time.elapsed() >= repeat_interval {
let now = std::time::Instant::now();
for key in &currently_held_keys {
-
if is_modifier_key(key) {
+
if is_modifier_key(*key) {
continue;
}
if let Some(press_time) = key_press_times.get(key) {
···
}
});
+
#[cfg(feature = "tray")]
loop {
use std::mem;
use winapi::um::winuser;
···
}
}
}
-
}
-
-
fn is_modifier_key(key: &Keycode) -> bool {
-
matches!(
-
key,
-
Keycode::LShift
-
| Keycode::RShift
-
| Keycode::LControl
-
| Keycode::RControl
-
| Keycode::RAlt
-
| Keycode::LAlt
-
| Keycode::RMeta
-
| Keycode::LMeta
-
| Keycode::CapsLock
-
)
+
#[cfg(not(feature = "tray"))]
+
_t.join().unwrap();
}