macos: enable running lan-mouse on macos (#42)

* macos: initial support

- adapted conditional compilation
- moved lan-mouse socket to ~/Library/Caches/lan-mouse-socket.sock instead of XDG_RUNTIME_DIR
- support for mouse input emulation
TODO: Keycode translation, input capture
This commit is contained in:
Ferdinand Schober
2023-12-09 01:35:08 +01:00
committed by GitHub
parent 5a7e0cf89c
commit e3f9947284
13 changed files with 419 additions and 21 deletions

View File

@@ -1,14 +1,17 @@
#[cfg(windows)]
pub mod windows;
#[cfg(all(unix, feature = "x11"))]
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
pub mod x11;
#[cfg(all(unix, feature = "wayland"))]
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
pub mod wlroots;
#[cfg(all(unix, feature = "xdg_desktop_portal"))]
#[cfg(all(unix, feature = "xdg_desktop_portal", not(target_os = "macos")))]
pub mod xdg_desktop_portal;
#[cfg(all(unix, feature = "libei"))]
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
pub mod libei;
#[cfg(target_os = "macos")]
pub mod macos;

View File

@@ -0,0 +1,221 @@
use crate::client::{ClientEvent, ClientHandle};
use crate::consumer::EventConsumer;
use crate::event::{Event, KeyboardEvent, PointerEvent};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use core_graphics::display::CGPoint;
use core_graphics::event::{
CGEvent, CGEventTapLocation, CGEventType, CGMouseButton, ScrollEventUnit,
};
use core_graphics::event_source::{CGEventSource, CGEventSourceStateID};
use std::ops::{Index, IndexMut};
pub struct MacOSConsumer {
pub event_source: CGEventSource,
button_state: ButtonState,
}
struct ButtonState {
left: bool,
right: bool,
center: bool,
}
impl Index<CGMouseButton> for ButtonState {
type Output = bool;
fn index(&self, index: CGMouseButton) -> &Self::Output {
match index {
CGMouseButton::Left => &self.left,
CGMouseButton::Right => &self.right,
CGMouseButton::Center => &self.center,
}
}
}
impl IndexMut<CGMouseButton> for ButtonState {
fn index_mut(&mut self, index: CGMouseButton) -> &mut Self::Output {
match index {
CGMouseButton::Left => &mut self.left,
CGMouseButton::Right => &mut self.right,
CGMouseButton::Center => &mut self.center,
}
}
}
unsafe impl Send for MacOSConsumer {}
impl MacOSConsumer {
pub fn new() -> Result<Self> {
let event_source = match CGEventSource::new(CGEventSourceStateID::CombinedSessionState) {
Ok(e) => e,
Err(_) => return Err(anyhow!("event source creation failed!")),
};
let button_state = ButtonState {
left: false,
right: false,
center: false,
};
Ok(Self {
event_source,
button_state,
})
}
fn get_mouse_location(&self) -> Option<CGPoint> {
let event: CGEvent = CGEvent::new(self.event_source.clone()).ok()?;
Some(event.location())
}
}
#[async_trait]
impl EventConsumer for MacOSConsumer {
async fn consume(&mut self, event: Event, _client_handle: ClientHandle) {
match event {
Event::Pointer(pointer_event) => match pointer_event {
PointerEvent::Motion {
time: _,
relative_x,
relative_y,
} => {
let mut mouse_location = match self.get_mouse_location() {
Some(l) => l,
None => {
log::warn!("could not get mouse location!");
return;
}
};
mouse_location.x += relative_x;
mouse_location.y += relative_y;
let mut event_type = CGEventType::MouseMoved;
if self.button_state.left {
event_type = CGEventType::LeftMouseDragged
} else if self.button_state.right {
event_type = CGEventType::RightMouseDragged
} else if self.button_state.center {
event_type = CGEventType::OtherMouseDragged
};
let event = match CGEvent::new_mouse_event(
self.event_source.clone(),
event_type,
mouse_location,
CGMouseButton::Left,
) {
Ok(e) => e,
Err(_) => {
log::warn!("mouse event creation failed!");
return;
}
};
event.post(CGEventTapLocation::HID);
}
PointerEvent::Button {
time: _,
button,
state,
} => {
let (event_type, mouse_button) = match (button, state) {
(b, 1) if b == crate::event::BTN_LEFT => {
(CGEventType::LeftMouseDown, CGMouseButton::Left)
}
(b, 0) if b == crate::event::BTN_LEFT => {
(CGEventType::LeftMouseUp, CGMouseButton::Left)
}
(b, 1) if b == crate::event::BTN_RIGHT => {
(CGEventType::RightMouseDown, CGMouseButton::Right)
}
(b, 0) if b == crate::event::BTN_RIGHT => {
(CGEventType::RightMouseUp, CGMouseButton::Right)
}
(b, 1) if b == crate::event::BTN_MIDDLE => {
(CGEventType::OtherMouseDown, CGMouseButton::Center)
}
(b, 0) if b == crate::event::BTN_MIDDLE => {
(CGEventType::OtherMouseUp, CGMouseButton::Center)
}
_ => {
log::warn!("invalid button event: {button},{state}");
return;
}
};
// store button state
self.button_state[mouse_button] = if state == 1 { true } else { false };
let location = self.get_mouse_location().unwrap();
let event = match CGEvent::new_mouse_event(
self.event_source.clone(),
event_type,
location,
mouse_button,
) {
Ok(e) => e,
Err(()) => {
log::warn!("mouse event creation failed!");
return;
}
};
event.post(CGEventTapLocation::HID);
}
PointerEvent::Axis {
time: _,
axis,
value,
} => {
let value = value as i32 / 10; // FIXME: high precision scroll events
let (count, wheel1, wheel2, wheel3) = match axis {
0 => (1, value, 0, 0), // 0 = vertical => 1 scroll wheel device (y axis)
1 => (2, 0, value, 0), // 1 = horizontal => 2 scroll wheel devices (y, x) -> (0, x)
_ => {
log::warn!("invalid scroll event: {axis}, {value}");
return;
}
};
let event = match CGEvent::new_scroll_event(
self.event_source.clone(),
ScrollEventUnit::LINE,
count,
wheel1,
wheel2,
wheel3,
) {
Ok(e) => e,
Err(()) => {
log::warn!("scroll event creation failed!");
return;
}
};
event.post(CGEventTapLocation::HID);
}
PointerEvent::Frame { .. } => {}
},
Event::Keyboard(keyboard_event) => match keyboard_event {
KeyboardEvent::Key { .. } => {
/*
let code = CGKeyCode::from_le(key as u16);
let event = match CGEvent::new_keyboard_event(
self.event_source.clone(),
code,
match state { 1 => true, _ => false }
) {
Ok(e) => e,
Err(_) => {
log::warn!("unable to create key event");
return
}
};
event.post(CGEventTapLocation::HID);
*/
}
KeyboardEvent::Modifiers { .. } => {}
},
Event::Release() => {}
Event::Ping() => {}
Event::Pong() => {}
}
}
async fn notify(&mut self, _client_event: ClientEvent) {}
async fn destroy(&mut self) {}
}

View File

@@ -1,8 +1,10 @@
#[cfg(all(unix, feature = "libei"))]
#[cfg(all(unix, feature = "libei", not(target_os = "macos")))]
pub mod libei;
#[cfg(all(unix, feature = "wayland"))]
#[cfg(target_os = "macos")]
pub mod macos;
#[cfg(all(unix, feature = "wayland", not(target_os = "macos")))]
pub mod wayland;
#[cfg(windows)]
pub mod windows;
#[cfg(all(unix, feature = "x11"))]
#[cfg(all(unix, feature = "x11", not(target_os = "macos")))]
pub mod x11;

View File

@@ -0,0 +1,28 @@
use crate::client::{ClientEvent, ClientHandle};
use crate::event::Event;
use crate::producer::EventProducer;
use futures_core::Stream;
use std::task::{Context, Poll};
use std::{io, pin::Pin};
pub struct MacOSProducer;
impl MacOSProducer {
pub fn new() -> Self {
Self {}
}
}
impl Stream for MacOSProducer {
type Item = io::Result<(ClientHandle, Event)>;
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Poll::Pending
}
}
impl EventProducer for MacOSProducer {
fn notify(&mut self, _event: ClientEvent) {}
fn release(&mut self) {}
}

View File

@@ -1,7 +1,7 @@
use async_trait::async_trait;
use std::future;
#[cfg(unix)]
#[cfg(all(unix, not(target_os = "macos")))]
use std::env;
use crate::{
@@ -11,7 +11,7 @@ use crate::{
};
use anyhow::Result;
#[cfg(unix)]
#[cfg(all(unix, not(target_os = "macos")))]
#[derive(Debug)]
enum Backend {
Wlroots,
@@ -37,7 +37,10 @@ pub async fn create() -> Result<Box<dyn EventConsumer>> {
#[cfg(windows)]
return Ok(Box::new(consumer::windows::WindowsConsumer::new()));
#[cfg(unix)]
#[cfg(target_os = "macos")]
return Ok(Box::new(consumer::macos::MacOSConsumer::new()?));
#[cfg(all(unix, not(target_os = "macos")))]
let backend = match env::var("XDG_SESSION_TYPE") {
Ok(session_type) => match session_type.as_str() {
"x11" => {
@@ -87,7 +90,7 @@ pub async fn create() -> Result<Box<dyn EventConsumer>> {
}
};
#[cfg(unix)]
#[cfg(all(unix, not(target_os = "macos")))]
match backend {
Backend::Libei => {
#[cfg(not(feature = "libei"))]

View File

@@ -3,6 +3,11 @@ use std::{
fmt::{self, Display},
};
// FIXME
pub(crate) const BTN_LEFT: u32 = 0x110;
pub(crate) const BTN_RIGHT: u32 = 0x111;
pub(crate) const BTN_MIDDLE: u32 = 0x112;
#[derive(Debug, Clone, Copy)]
pub enum PointerEvent {
Motion {

View File

@@ -126,7 +126,7 @@ pub struct FrontendListener {
}
impl FrontendListener {
#[cfg(unix)]
#[cfg(all(unix, not(target_os = "macos")))]
pub fn socket_path() -> Result<PathBuf> {
let xdg_runtime_dir = match env::var("XDG_RUNTIME_DIR") {
Ok(d) => d,
@@ -136,6 +136,20 @@ impl FrontendListener {
Ok(xdg_runtime_dir.join("lan-mouse-socket.sock"))
}
#[cfg(all(unix, target_os = "macos"))]
pub fn socket_path() -> Result<PathBuf> {
let home = match env::var("HOME") {
Ok(d) => d,
Err(e) => return Err(anyhow!("could not find HOME: {e}")),
};
let home = Path::new(home.as_str());
let path = home
.join("Library")
.join("Caches")
.join("lan-mouse-socket.sock");
Ok(path)
}
pub async fn new() -> Option<Result<Self>> {
#[cfg(unix)]
let (socket_path, listener) = {

View File

@@ -9,10 +9,10 @@ use crate::{
event::Event,
};
#[cfg(unix)]
#[cfg(all(unix, not(target_os = "macos")))]
use std::env;
#[cfg(unix)]
#[cfg(all(unix, not(target_os = "macos")))]
enum Backend {
LayerShell,
Libei,
@@ -20,10 +20,13 @@ enum Backend {
}
pub async fn create() -> Result<Box<dyn EventProducer>> {
#[cfg(target_os = "macos")]
return Ok(Box::new(producer::macos::MacOSProducer::new()));
#[cfg(windows)]
return Ok(Box::new(producer::windows::WindowsProducer::new()));
#[cfg(unix)]
#[cfg(all(unix, not(target_os = "macos")))]
let backend = match env::var("XDG_SESSION_TYPE") {
Ok(session_type) => match session_type.as_str() {
"x11" => {
@@ -56,7 +59,7 @@ pub async fn create() -> Result<Box<dyn EventProducer>> {
}
};
#[cfg(unix)]
#[cfg(all(unix, not(target_os = "macos")))]
match backend {
Backend::X11 => {
#[cfg(not(feature = "x11"))]