diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 38187011..a99ed550 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -39,9 +39,23 @@ jobs: name: lan-mouse-windows path: target/release/lan-mouse.exe + macos-release-build: + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - name: install dependencies + run: brew install gtk4 libadwaita + - name: Release Build + run: cargo build --release + - name: Upload build artifact + uses: actions/upload-artifact@v3 + with: + name: lan-mouse-macos + path: target/release/lan-mouse + pre-release: name: "Pre Release" - needs: [windows-release-build, linux-release-build] + needs: [windows-release-build, linux-release-build, macos-release-build] runs-on: "ubuntu-latest" steps: - name: Download build artifacts @@ -56,3 +70,4 @@ jobs: files: | lan-mouse-linux/lan-mouse lan-mouse-windows/lan-mouse.exe + lan-mouse-macos/lan-mouse diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 16a0fbbc..7b5c3ebb 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -46,3 +46,19 @@ jobs: with: name: lan-mouse-windows path: target/debug/lan-mouse.exe + + build-macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - name: install dependencies + run: brew install gtk4 libadwaita + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose + - name: Upload build artifact + uses: actions/upload-artifact@v3 + with: + name: lan-mouse-macos + path: target/debug/lan-mouse diff --git a/.github/workflows/tagged-release.yml b/.github/workflows/tagged-release.yml index 6d334724..38063a5e 100644 --- a/.github/workflows/tagged-release.yml +++ b/.github/workflows/tagged-release.yml @@ -35,9 +35,23 @@ jobs: name: lan-mouse-windows path: target/release/lan-mouse.exe + macos-release-build: + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - name: install dependencies + run: brew install gtk4 libadwaita + - name: Release Build + run: cargo build --release + - name: Upload build artifact + uses: actions/upload-artifact@v3 + with: + name: lan-mouse-macos + path: target/release/lan-mouse + tagged-release: name: "Tagged Release" - needs: [windows-release-build, linux-release-build] + needs: [windows-release-build, linux-release-build, macos-release-build] runs-on: "ubuntu-latest" steps: - name: Download build artifacts @@ -50,3 +64,4 @@ jobs: files: | lan-mouse-linux/lan-mouse lan-mouse-windows/lan-mouse.exe + lan-mouse-macos/lan-mouse diff --git a/Cargo.lock b/Cargo.lock index 75ee8327..ba327872 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -375,6 +375,46 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "core-graphics" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "970a29baf4110c26fedbc7f82107d42c23f7e88e404c4577ed73fe99ff85a212" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + [[package]] name = "cpufeatures" version = "0.2.9" @@ -549,6 +589,33 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.37", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + [[package]] name = "form_urlencoded" version = "1.2.0" @@ -1075,6 +1142,7 @@ dependencies = [ "ashpd", "async-trait", "clap", + "core-graphics", "env_logger", "futures", "futures-core", diff --git a/Cargo.toml b/Cargo.toml index 077b6176..806fff7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,18 +29,23 @@ futures-core = "0.3.28" futures = "0.3.28" clap = { version="4.4.11", features = ["derive"] } -[target.'cfg(unix)'.dependencies] +[target.'cfg(all(unix, not(target_os="macos")))'.dependencies] wayland-client = { version="0.30.2", optional = true } wayland-protocols = { version="0.30.0", features=["client", "staging", "unstable"], optional = true } wayland-protocols-wlr = { version="0.1.0", features=["client"], optional = true } wayland-protocols-misc = { version="0.1.0", features=["client"], optional = true } wayland-protocols-plasma = { version="0.1.0", features=["client"], optional = true } x11 = { version = "2.21.0", features = ["xlib", "xtest"], optional = true } -gtk = { package = "gtk4", version = "0.7.2", features = ["v4_6"], optional = true } -adw = { package = "libadwaita", version = "0.5.2", features = ["v1_1"], optional = true } ashpd = { version = "0.6.2", default-features = false, features = ["tokio"], optional = true } reis = { git = "https://github.com/ids1024/reis", features = [ "tokio" ], optional = true } +[target.'cfg(unix)'.dependencies] +gtk = { package = "gtk4", version = "0.7.2", features = ["v4_6"], optional = true } +adw = { package = "libadwaita", version = "0.5.2", features = ["v1_1"], optional = true } + +[target.'cfg(target_os="macos")'.dependencies] +core-graphics = { version = "0.23", features = ["highsierra"] } + [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.9", features = ["winuser"] } diff --git a/src/backend/consumer.rs b/src/backend/consumer.rs index c5f2a477..2aae0fc1 100644 --- a/src/backend/consumer.rs +++ b/src/backend/consumer.rs @@ -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; diff --git a/src/backend/consumer/macos.rs b/src/backend/consumer/macos.rs new file mode 100644 index 00000000..76d0fdfc --- /dev/null +++ b/src/backend/consumer/macos.rs @@ -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 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 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 { + 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 { + 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) {} +} diff --git a/src/backend/producer.rs b/src/backend/producer.rs index 2f9f736a..eac1f69a 100644 --- a/src/backend/producer.rs +++ b/src/backend/producer.rs @@ -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; diff --git a/src/backend/producer/macos.rs b/src/backend/producer/macos.rs new file mode 100644 index 00000000..1f9e5ce4 --- /dev/null +++ b/src/backend/producer/macos.rs @@ -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> { + Poll::Pending + } +} + +impl EventProducer for MacOSProducer { + fn notify(&mut self, _event: ClientEvent) {} + + fn release(&mut self) {} +} diff --git a/src/consumer.rs b/src/consumer.rs index 3bb28fec..1c1b6abb 100644 --- a/src/consumer.rs +++ b/src/consumer.rs @@ -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> { #[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> { } }; - #[cfg(unix)] + #[cfg(all(unix, not(target_os = "macos")))] match backend { Backend::Libei => { #[cfg(not(feature = "libei"))] diff --git a/src/event.rs b/src/event.rs index 00032da8..b5b29287 100644 --- a/src/event.rs +++ b/src/event.rs @@ -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 { diff --git a/src/frontend.rs b/src/frontend.rs index 5f21c7d8..95d6574c 100644 --- a/src/frontend.rs +++ b/src/frontend.rs @@ -126,7 +126,7 @@ pub struct FrontendListener { } impl FrontendListener { - #[cfg(unix)] + #[cfg(all(unix, not(target_os = "macos")))] pub fn socket_path() -> Result { 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 { + 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> { #[cfg(unix)] let (socket_path, listener) = { diff --git a/src/producer.rs b/src/producer.rs index e4340dcc..8213f60a 100644 --- a/src/producer.rs +++ b/src/producer.rs @@ -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> { + #[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> { } }; - #[cfg(unix)] + #[cfg(all(unix, not(target_os = "macos")))] match backend { Backend::X11 => { #[cfg(not(feature = "x11"))]