From 557da1b0bc3b335c6df2e0cb76f5cb60fdfa7b63 Mon Sep 17 00:00:00 2001 From: unsecretised Date: Thu, 8 Jan 2026 13:13:40 +0800 Subject: [PATCH] Add haptic feedback feature --- Cargo.lock | 4 + Cargo.toml | 2 + build.rs | 5 ++ src/app/tile/update.rs | 7 ++ src/config.rs | 2 + src/haptics.rs | 162 +++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + 7 files changed, 183 insertions(+) create mode 100644 build.rs create mode 100644 src/haptics.rs diff --git a/Cargo.lock b/Cargo.lock index 314dc09..3b39e37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2385,7 +2385,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.10.0", + "block2 0.6.2", "dispatch2", + "libc", "objc2 0.6.3", ] @@ -3089,7 +3091,9 @@ dependencies = [ "objc2 0.6.3", "objc2-app-kit 0.3.2", "objc2-application-services", + "objc2-core-foundation", "objc2-foundation 0.3.2", + "once_cell", "rand", "rayon", "serde", diff --git a/Cargo.toml b/Cargo.toml index 669ae9c..4c807da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,9 @@ objc2-application-services = { version = "0.3.2", default-features = false, feat "HIServices", "Processes", ] } +objc2-core-foundation = "0.3.2" objc2-foundation = { version = "0.3.2", features = ["NSString"] } +once_cell = "1.21.3" rand = "0.9.2" rayon = "1.11.0" serde = { version = "1.0.228", features = ["derive"] } diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..a4b0b05 --- /dev/null +++ b/build.rs @@ -0,0 +1,5 @@ +fn main() { + println!("cargo:rustc-link-search=framework=/System/Library/PrivateFrameworks"); + println!("cargo:rustc-link-lib=framework=IOKit"); + println!("cargo:rustc-link-lib=framework=MultitouchSupport"); +} diff --git a/src/app/tile/update.rs b/src/app/tile/update.rs index 13cabe3..198b56a 100644 --- a/src/app/tile/update.rs +++ b/src/app/tile/update.rs @@ -20,6 +20,8 @@ use crate::app::tile::elm::default_app_paths; use crate::calculator::Expression; use crate::commands::Function; use crate::config::Config; +use crate::haptics::HapticPattern; +use crate::haptics::perform_haptic; use crate::utils::get_installed_apps; use crate::{ app::{Message, Page, tile::Tile}, @@ -36,6 +38,11 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task { } Message::SearchQueryChanged(input, id) => { + #[cfg(target_os = "macos")] + if tile.config.haptic_feedback { + perform_haptic(HapticPattern::Alignment); + } + tile.query_lc = input.trim().to_lowercase(); tile.query = input; let prev_size = tile.results.len(); diff --git a/src/config.rs b/src/config.rs index 657de41..2a2bcaf 100644 --- a/src/config.rs +++ b/src/config.rs @@ -22,6 +22,7 @@ pub struct Config { pub theme: Theme, pub placeholder: String, pub search_url: String, + pub haptic_feedback: bool, pub shells: Vec, } @@ -35,6 +36,7 @@ impl Default for Config { theme: Theme::default(), placeholder: String::from("Time to be productive!"), search_url: "https://google.com/search?q=%s".to_string(), + haptic_feedback: false, shells: vec![], } } diff --git a/src/haptics.rs b/src/haptics.rs new file mode 100644 index 0000000..c9d5d863 --- /dev/null +++ b/src/haptics.rs @@ -0,0 +1,162 @@ +#![allow(non_camel_case_types)] + +use objc2_core_foundation::{CFNumber, CFNumberType, CFRetained, CFString, CFType}; +use once_cell::sync::OnceCell; +use std::ffi::{CStr, c_char, c_void}; + +#[allow(dead_code)] +#[derive(Copy, Clone, Debug)] +pub enum HapticPattern { + Generic, + Alignment, + LevelChange, +} + +unsafe extern "C" { + unsafe fn CFRelease(cf: *mut CFType); +} + +#[inline] +fn pattern_index(pattern: HapticPattern) -> i32 { + match pattern { + HapticPattern::Generic => 0, + HapticPattern::Alignment => 1, + HapticPattern::LevelChange => 2, + } +} + +type kern_return_t = i32; +type io_object_t = u32; +type io_iterator_t = u32; +type io_registry_entry_t = u32; +type mach_port_t = u32; + +unsafe extern "C" { + fn IOServiceMatching(name: *const c_char) -> *mut CFType; + fn IOServiceGetMatchingServices( + master: mach_port_t, + matching: *mut CFType, + iter: *mut io_iterator_t, + ) -> kern_return_t; + fn IOIteratorNext(iter: io_iterator_t) -> io_object_t; + fn IOObjectRelease(obj: io_object_t) -> kern_return_t; + fn IORegistryEntryCreateCFProperty( + entry: io_registry_entry_t, + key: *mut CFString, + allocator: *const c_void, + options: u32, + ) -> *mut CFType; + + fn MTActuatorCreateFromDeviceID(device_id: u64) -> *mut CFType; + fn MTActuatorOpen(actuator: *mut CFType) -> i32; // IOReturn + fn MTActuatorIsOpen(actuator: *mut CFType) -> bool; + fn MTActuatorActuate(actuator: *mut CFType, pattern: i32, unk: i32, f1: f32, f2: f32) -> i32; + + fn CFGetTypeID(cf: *mut CFType) -> usize; + fn CFNumberGetTypeID() -> usize; + fn CFNumberGetValue(number: *mut CFNumber, theType: i32, valuePtr: *mut u64) -> bool; +} + +#[inline] +fn k_iomain_port_default() -> mach_port_t { + 0 +} + +struct MtsState { + actuators: Vec<*mut CFType>, +} + +unsafe impl Send for MtsState {} +unsafe impl Sync for MtsState {} + +impl MtsState { + fn open_default_or_all() -> Option { + let mut iter: io_iterator_t = 0; + unsafe { + let name = CStr::from_bytes_with_nul_unchecked(b"AppleMultitouchDevice\0"); + let matching = IOServiceMatching(name.as_ptr()); + if matching.is_null() { + return None; + } + if IOServiceGetMatchingServices(k_iomain_port_default(), matching, &mut iter) != 0 { + return None; + } + } + + let key = CFString::from_str("Multitouch ID"); + let mut actuators: Vec<*mut CFType> = Vec::new(); + + unsafe { + loop { + let dev = IOIteratorNext(iter); + if dev == 0 { + break; + } + + let id_ref = IORegistryEntryCreateCFProperty( + dev, + CFRetained::::as_ptr(&key).as_ptr(), + std::ptr::null(), + 0, + ); + + if !id_ref.is_null() && CFGetTypeID(id_ref) == CFNumberGetTypeID() { + let mut device_id: u64 = 0; + if CFNumberGetValue( + id_ref as *mut CFNumber, + CFNumberType::SInt64Type.0 as i32, + &mut device_id as *mut u64, + ) { + let act = MTActuatorCreateFromDeviceID(device_id); + if !act.is_null() { + if MTActuatorOpen(act) == 0 { + actuators.push(act); + } else { + CFRelease(act); + } + } + } + } + + if !id_ref.is_null() { + CFRelease(id_ref); + } + IOObjectRelease(dev); + } + + if iter != 0 { + IOObjectRelease(iter); + } + } + + if actuators.is_empty() { + None + } else { + Some(Self { actuators }) + } + } +} + +static MTS: OnceCell> = OnceCell::new(); + +fn mts_state() -> Option<&'static MtsState> { + MTS.get_or_init(|| MtsState::open_default_or_all()).as_ref() +} + +pub fn perform_haptic(pattern: HapticPattern) -> bool { + if let Some(state) = mts_state() { + let pat = pattern_index(pattern); + let mut any_ok = false; + unsafe { + for &act in &state.actuators { + if !act.is_null() && MTActuatorIsOpen(act) { + let kr = MTActuatorActuate(act, pat, 0, 0.0, 0.0); + any_ok |= kr == 0; + } + } + } + any_ok + } else { + false + } +} diff --git a/src/main.rs b/src/main.rs index 741044c..e575406 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod calculator; mod clipboard; mod commands; mod config; +mod haptics; mod macos; mod utils;