Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
5 changes: 5 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -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");
}
7 changes: 7 additions & 0 deletions src/app/tile/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -36,6 +38,11 @@ pub fn handle_update(tile: &mut Tile, message: Message) -> Task<Message> {
}

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();
Expand Down
2 changes: 2 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ pub struct Config {
pub theme: Theme,
pub placeholder: String,
pub search_url: String,
pub haptic_feedback: bool,
pub shells: Vec<Shelly>,
}

Expand All @@ -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![],
}
}
Expand Down
162 changes: 162 additions & 0 deletions src/haptics.rs
Original file line number Diff line number Diff line change
@@ -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<Self> {
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::<CFString>::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<Option<MtsState>> = 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
}
}
1 change: 1 addition & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod calculator;
mod clipboard;
mod commands;
mod config;
mod haptics;
mod macos;
mod utils;

Expand Down