Skip to content

Commit 557da1b

Browse files
committed
Add haptic feedback feature
1 parent 193e1c6 commit 557da1b

File tree

7 files changed

+183
-0
lines changed

7 files changed

+183
-0
lines changed

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ objc2-application-services = { version = "0.3.2", default-features = false, feat
1616
"HIServices",
1717
"Processes",
1818
] }
19+
objc2-core-foundation = "0.3.2"
1920
objc2-foundation = { version = "0.3.2", features = ["NSString"] }
21+
once_cell = "1.21.3"
2022
rand = "0.9.2"
2123
rayon = "1.11.0"
2224
serde = { version = "1.0.228", features = ["derive"] }

build.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
fn main() {
2+
println!("cargo:rustc-link-search=framework=/System/Library/PrivateFrameworks");
3+
println!("cargo:rustc-link-lib=framework=IOKit");
4+
println!("cargo:rustc-link-lib=framework=MultitouchSupport");
5+
}

src/app/tile/update.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ use crate::app::tile::elm::default_app_paths;
2020
use crate::calculator::Expression;
2121
use crate::commands::Function;
2222
use crate::config::Config;
23+
use crate::haptics::HapticPattern;
24+
use crate::haptics::perform_haptic;
2325
use crate::utils::get_installed_apps;
2426
use crate::{
2527
app::{Message, Page, tile::Tile},
@@ -36,6 +38,11 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
3638
}
3739

3840
Message::SearchQueryChanged(input, id) => {
41+
#[cfg(target_os = "macos")]
42+
if tile.config.haptic_feedback {
43+
perform_haptic(HapticPattern::Alignment);
44+
}
45+
3946
tile.query_lc = input.trim().to_lowercase();
4047
tile.query = input;
4148
let prev_size = tile.results.len();

src/config.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub struct Config {
2222
pub theme: Theme,
2323
pub placeholder: String,
2424
pub search_url: String,
25+
pub haptic_feedback: bool,
2526
pub shells: Vec<Shelly>,
2627
}
2728

@@ -35,6 +36,7 @@ impl Default for Config {
3536
theme: Theme::default(),
3637
placeholder: String::from("Time to be productive!"),
3738
search_url: "https://google.com/search?q=%s".to_string(),
39+
haptic_feedback: false,
3840
shells: vec![],
3941
}
4042
}

src/haptics.rs

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
#![allow(non_camel_case_types)]
2+
3+
use objc2_core_foundation::{CFNumber, CFNumberType, CFRetained, CFString, CFType};
4+
use once_cell::sync::OnceCell;
5+
use std::ffi::{CStr, c_char, c_void};
6+
7+
#[allow(dead_code)]
8+
#[derive(Copy, Clone, Debug)]
9+
pub enum HapticPattern {
10+
Generic,
11+
Alignment,
12+
LevelChange,
13+
}
14+
15+
unsafe extern "C" {
16+
unsafe fn CFRelease(cf: *mut CFType);
17+
}
18+
19+
#[inline]
20+
fn pattern_index(pattern: HapticPattern) -> i32 {
21+
match pattern {
22+
HapticPattern::Generic => 0,
23+
HapticPattern::Alignment => 1,
24+
HapticPattern::LevelChange => 2,
25+
}
26+
}
27+
28+
type kern_return_t = i32;
29+
type io_object_t = u32;
30+
type io_iterator_t = u32;
31+
type io_registry_entry_t = u32;
32+
type mach_port_t = u32;
33+
34+
unsafe extern "C" {
35+
fn IOServiceMatching(name: *const c_char) -> *mut CFType;
36+
fn IOServiceGetMatchingServices(
37+
master: mach_port_t,
38+
matching: *mut CFType,
39+
iter: *mut io_iterator_t,
40+
) -> kern_return_t;
41+
fn IOIteratorNext(iter: io_iterator_t) -> io_object_t;
42+
fn IOObjectRelease(obj: io_object_t) -> kern_return_t;
43+
fn IORegistryEntryCreateCFProperty(
44+
entry: io_registry_entry_t,
45+
key: *mut CFString,
46+
allocator: *const c_void,
47+
options: u32,
48+
) -> *mut CFType;
49+
50+
fn MTActuatorCreateFromDeviceID(device_id: u64) -> *mut CFType;
51+
fn MTActuatorOpen(actuator: *mut CFType) -> i32; // IOReturn
52+
fn MTActuatorIsOpen(actuator: *mut CFType) -> bool;
53+
fn MTActuatorActuate(actuator: *mut CFType, pattern: i32, unk: i32, f1: f32, f2: f32) -> i32;
54+
55+
fn CFGetTypeID(cf: *mut CFType) -> usize;
56+
fn CFNumberGetTypeID() -> usize;
57+
fn CFNumberGetValue(number: *mut CFNumber, theType: i32, valuePtr: *mut u64) -> bool;
58+
}
59+
60+
#[inline]
61+
fn k_iomain_port_default() -> mach_port_t {
62+
0
63+
}
64+
65+
struct MtsState {
66+
actuators: Vec<*mut CFType>,
67+
}
68+
69+
unsafe impl Send for MtsState {}
70+
unsafe impl Sync for MtsState {}
71+
72+
impl MtsState {
73+
fn open_default_or_all() -> Option<Self> {
74+
let mut iter: io_iterator_t = 0;
75+
unsafe {
76+
let name = CStr::from_bytes_with_nul_unchecked(b"AppleMultitouchDevice\0");
77+
let matching = IOServiceMatching(name.as_ptr());
78+
if matching.is_null() {
79+
return None;
80+
}
81+
if IOServiceGetMatchingServices(k_iomain_port_default(), matching, &mut iter) != 0 {
82+
return None;
83+
}
84+
}
85+
86+
let key = CFString::from_str("Multitouch ID");
87+
let mut actuators: Vec<*mut CFType> = Vec::new();
88+
89+
unsafe {
90+
loop {
91+
let dev = IOIteratorNext(iter);
92+
if dev == 0 {
93+
break;
94+
}
95+
96+
let id_ref = IORegistryEntryCreateCFProperty(
97+
dev,
98+
CFRetained::<CFString>::as_ptr(&key).as_ptr(),
99+
std::ptr::null(),
100+
0,
101+
);
102+
103+
if !id_ref.is_null() && CFGetTypeID(id_ref) == CFNumberGetTypeID() {
104+
let mut device_id: u64 = 0;
105+
if CFNumberGetValue(
106+
id_ref as *mut CFNumber,
107+
CFNumberType::SInt64Type.0 as i32,
108+
&mut device_id as *mut u64,
109+
) {
110+
let act = MTActuatorCreateFromDeviceID(device_id);
111+
if !act.is_null() {
112+
if MTActuatorOpen(act) == 0 {
113+
actuators.push(act);
114+
} else {
115+
CFRelease(act);
116+
}
117+
}
118+
}
119+
}
120+
121+
if !id_ref.is_null() {
122+
CFRelease(id_ref);
123+
}
124+
IOObjectRelease(dev);
125+
}
126+
127+
if iter != 0 {
128+
IOObjectRelease(iter);
129+
}
130+
}
131+
132+
if actuators.is_empty() {
133+
None
134+
} else {
135+
Some(Self { actuators })
136+
}
137+
}
138+
}
139+
140+
static MTS: OnceCell<Option<MtsState>> = OnceCell::new();
141+
142+
fn mts_state() -> Option<&'static MtsState> {
143+
MTS.get_or_init(|| MtsState::open_default_or_all()).as_ref()
144+
}
145+
146+
pub fn perform_haptic(pattern: HapticPattern) -> bool {
147+
if let Some(state) = mts_state() {
148+
let pat = pattern_index(pattern);
149+
let mut any_ok = false;
150+
unsafe {
151+
for &act in &state.actuators {
152+
if !act.is_null() && MTActuatorIsOpen(act) {
153+
let kr = MTActuatorActuate(act, pat, 0, 0.0, 0.0);
154+
any_ok |= kr == 0;
155+
}
156+
}
157+
}
158+
any_ok
159+
} else {
160+
false
161+
}
162+
}

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod calculator;
33
mod clipboard;
44
mod commands;
55
mod config;
6+
mod haptics;
67
mod macos;
78
mod utils;
89

0 commit comments

Comments
 (0)