From bef1bb64537200f3f4d820f73ca7abcdd1fbd4d8 Mon Sep 17 00:00:00 2001 From: Vadim Date: Wed, 18 Sep 2024 22:05:23 +0300 Subject: [PATCH] Add support Time and Input layout for MacOS (#5) Co-authored-by: vadimsuhanov --- .github/workflows/pre-release.yml | 23 +++++ .github/workflows/test.yml | 12 +++ Cargo.lock | 141 ++++++++++++++++++++++++++++-- Cargo.toml | 7 +- README.md | 18 ++-- build.rs | 3 + src/data_type.rs | 7 ++ src/main.rs | 13 ++- src/providers/layout.rs | 6 ++ src/providers/layout/macos.rs | 109 +++++++++++++++++++++++ 10 files changed, 323 insertions(+), 16 deletions(-) create mode 100644 src/providers/layout/macos.rs diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index 8f6dc3a..fa7fdd4 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -58,11 +58,34 @@ jobs: path: dist name: ubuntu + build-macos: + name: '[Macos] Build and Publish' + runs-on: macos-latest + + steps: + - uses: actions/checkout@v3 + + - name: Build + run: cargo build --release + + - name: Install cargo-make + uses: davidB/rust-cargo-make@v1 + + - name: Publish files + run: cargo make dist + + - name: Upload files + uses: actions/upload-artifact@v4 + with: + path: dist + name: macos + pre-release: name: Pre-Release needs: - build-windows - build-ubuntu + - build-macos runs-on: ubuntu-latest permissions: contents: 'write' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9c87cbe..2f16fec 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,3 +34,15 @@ jobs: - name: Run tests run: cargo test + + build-macos: + runs-on: macos-latest + + steps: + - uses: actions/checkout@v3 + + - name: Build + run: cargo build --release + + - name: Run tests + run: cargo test \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 068bb40..6dfa215 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -175,6 +175,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + [[package]] name = "blocking" version = "1.3.1" @@ -196,6 +202,12 @@ version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + [[package]] name = "cc" version = "1.0.79" @@ -232,11 +244,21 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crossbeam-utils" @@ -559,7 +581,7 @@ version = "2.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed3557a2dfc380c8f061189a01c6ae7348354e0c9886038dc6c171219c08eaff" dependencies = [ - "bitflags", + "bitflags 1.3.2", "libc", "libpulse-sys", "num-derive", @@ -586,6 +608,16 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.19" @@ -619,6 +651,17 @@ dependencies = [ "adler", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + [[package]] name = "mpris" version = "2.0.1" @@ -662,6 +705,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.31.1" @@ -689,6 +742,29 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.5", +] + [[package]] name = "pin-project-lite" version = "0.2.10" @@ -714,7 +790,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ "autocfg", - "bitflags", + "bitflags 1.3.2", "cfg-if", "concurrent-queue", "libc", @@ -747,7 +823,10 @@ version = "0.0.0" dependencies = [ "async-std", "chrono", + "core-foundation", + "core-foundation-sys", "hidapi", + "libc", "libpulse-binding", "mpris", "pulsectl-rs", @@ -776,6 +855,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "regex" version = "1.9.1" @@ -832,7 +920,7 @@ version = "0.37.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno", "io-lifetimes", "libc", @@ -846,6 +934,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.178" @@ -886,6 +980,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "slab" version = "0.4.8" @@ -996,7 +1099,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" dependencies = [ "libc", - "wasi", + "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] @@ -1008,7 +1111,27 @@ checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" dependencies = [ "autocfg", "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote 1.0.32", + "syn 2.0.27", ] [[package]] @@ -1115,6 +1238,12 @@ version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "wasm-bindgen" version = "0.2.87" diff --git a/Cargo.toml b/Cargo.toml index 0392d3d..cab2a88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ tracing = "0.1" tracing-subscriber = { version="0.3", features = ["env-filter"] } chrono = "0.4.26" hidapi = "2.4.0" -tokio = { version = "1.29.1", features = ["sync"] } +tokio = { version = "1.29.1", features = ["full"] } async-std = "1.7.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -26,6 +26,11 @@ libpulse-binding = "2.28.1" x11 = "2.21.0" mpris = "2.0.1" +[target.'cfg(target_os = "macos")'.dependencies] +core-foundation = "0.10" +core-foundation-sys = "0.8.7" +libc = "0.2" + [target.'cfg(target_os = "windows")'.dependencies] [dependencies.windows] version = "0.56" diff --git a/README.md b/README.md index 7ba6aa0..ca9a593 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,14 @@ Application is written in Rust which gives easy access to HID libraries, low-lev ## Supported platforms/providers -| | Windows | Linux | -| ------------ | ------------------ | ------------------------------- | -| Time | :heavy_check_mark: | :heavy_check_mark: | -| Volume | :heavy_check_mark: | :heavy_check_mark: (PulseAudio) | -| Input layout | :heavy_check_mark: | :heavy_check_mark: (X11) | -| Media info | :heavy_check_mark: | :heavy_check_mark: (D-Bus) | +| | Windows | Linux | macos | +| ------------ | ------------------ | ------------------------------- |--------------------| +| Time | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | +| Volume | :heavy_check_mark: | :heavy_check_mark: (PulseAudio) | | +| Input layout | :heavy_check_mark: | :heavy_check_mark: (X11) | :heavy_check_mark: | +| Media info | :heavy_check_mark: | :heavy_check_mark: (D-Bus) | | -MacOS is not supported, as I don't own any Apple devices, feel free to raise PRs. +MacOS is partially supported, as I don't own any Apple devices, feel free to raise PRs. ## How to run it @@ -57,6 +57,9 @@ When you verified that the application works with your keyboard, you can use `qm 2. Reconnect keyboard 3. Start `qmk-hid-host`, add it to autorun if needed +### MacOS +1. Start `qmk-hid-host`, add it to autorun if needed + ## Development 1. Install Rust @@ -64,7 +67,6 @@ When you verified that the application works with your keyboard, you can use `qm 3. If needed, edit `qmk-hid-host.json` in root folder and run again ## Changelog - - 2024-02-06 - add Linux support - 2024-01-21 - remove run as windows service, add silent version instead - 2024-01-02 - support RUST_LOG, run as windows service diff --git a/build.rs b/build.rs index a0f8225..10402ee 100644 --- a/build.rs +++ b/build.rs @@ -5,3 +5,6 @@ fn main() { #[cfg(target_os = "windows")] fn main() {} + +#[cfg(target_os = "macos")] +fn main() {} diff --git a/src/data_type.rs b/src/data_type.rs index f514101..acc6b21 100644 --- a/src/data_type.rs +++ b/src/data_type.rs @@ -1,3 +1,4 @@ +#[cfg(not (target_os = "macos"))] pub enum DataType { Time = 0xAA, // random value that does not conflict with VIA/VIAL, must match firmware Volume, @@ -5,3 +6,9 @@ pub enum DataType { MediaArtist, MediaTitle, } + +#[cfg(target_os = "macos")] +pub enum DataType { + Time = 0xAA, // random value that does not conflict with VIA/VIAL, must match firmware + Layout = 0xAC, +} \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index ddddb32..5b65879 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,11 @@ mod providers; use config::get_config; use keyboard::Keyboard; -use providers::{_base::Provider, layout::LayoutProvider, media::MediaProvider, time::TimeProvider, volume::VolumeProvider}; +#[cfg(not(target_os = "macos"))] +use providers::{_base::Provider, layout::LayoutProvider, time::TimeProvider, media::MediaProvider, volume::VolumeProvider}; + +#[cfg(target_os = "macos")] +use providers::{_base::Provider, layout::LayoutProvider, time::TimeProvider}; fn main() { let env_filter = tracing_subscriber::EnvFilter::builder() @@ -24,6 +28,13 @@ fn main() { let keyboard = Keyboard::new(config.device, config.reconnect_delay); let (connected_sender, data_sender) = keyboard.connect(); + #[cfg(target_os = "macos")] + let providers: Vec> = vec![ + TimeProvider::new(data_sender.clone(), connected_sender.clone()), + LayoutProvider::new(data_sender.clone(), connected_sender.clone(), config.layouts), + ]; + + #[cfg(not(target_os = "macos"))] let providers: Vec> = vec![ TimeProvider::new(data_sender.clone(), connected_sender.clone()), VolumeProvider::new(data_sender.clone(), connected_sender.clone()), diff --git a/src/providers/layout.rs b/src/providers/layout.rs index 3323e66..f1f3fe1 100644 --- a/src/providers/layout.rs +++ b/src/providers/layout.rs @@ -9,3 +9,9 @@ mod windows; #[cfg(target_os = "windows")] pub use self::windows::LayoutProvider; + +#[cfg(target_os = "macos")] +mod macos; + +#[cfg(target_os = "macos")] +pub use self::macos::LayoutProvider; diff --git a/src/providers/layout/macos.rs b/src/providers/layout/macos.rs new file mode 100644 index 0000000..9578f34 --- /dev/null +++ b/src/providers/layout/macos.rs @@ -0,0 +1,109 @@ +use crate::data_type::DataType; +use core_foundation::base::{CFRelease, TCFType}; +use core_foundation::string::{CFString, CFStringRef}; +use core_foundation_sys::runloop::{kCFRunLoopDefaultMode, CFRunLoopRunInMode}; +use libc::c_void; +use std::sync::{Arc, Mutex}; +use core_foundation_sys::base::Boolean; +use core_foundation_sys::date::CFTimeInterval; +use tokio::sync::{broadcast, mpsc}; + +use super::super::_base::Provider; + +#[link(name = "Carbon", kind = "framework")] +extern "C" { + fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut c_void; + fn TISGetInputSourceProperty(input_source: *mut c_void, key: CFStringRef) -> *mut CFStringRef; +} +fn get_keyboard_layout() -> Option { + unsafe { + + let layout_input_source = TISCopyCurrentKeyboardLayoutInputSource(); + if layout_input_source.is_null() { + return None; + } + + let k_tis_property_input_source_id = CFString::from_static_string("TISPropertyInputSourceID"); + + let layout_id_ptr = TISGetInputSourceProperty(layout_input_source, k_tis_property_input_source_id.as_concrete_TypeRef()); + CFRelease(layout_input_source); + + if layout_id_ptr.is_null() { + return None; + } + + let layout_id = layout_id_ptr as CFStringRef; + if layout_id.is_null() { + return None; + } + + let layout_string = CFString::wrap_under_get_rule(layout_id).to_string(); + + Some(layout_string) + } +} + +fn send_data(value: &String, layouts: &Vec, data_sender: &mpsc::Sender>) { + tracing::info!("new layout: '{0}', layout list: {1:?}", value, layouts); + if let Some(index) = layouts.into_iter().position(|r| r == value) { + let data = vec![DataType::Layout as u8, index as u8]; + data_sender.try_send(data).unwrap_or_else(|e| tracing::error!("{}", e)); + } +} + +pub struct LayoutProvider { + data_sender: mpsc::Sender>, + connected_sender: broadcast::Sender, + layouts: Vec, +} + +impl LayoutProvider { + pub fn new(data_sender: mpsc::Sender>, connected_sender: broadcast::Sender, layouts: Vec) -> Box { + let provider = LayoutProvider { + data_sender, + connected_sender, + layouts, + }; + Box::new(provider) + } +} + +impl Provider for LayoutProvider { + fn start(&self) { + tracing::info!("Layout Provider started"); + + let data_sender = self.data_sender.clone(); + let layouts = self.layouts.clone(); + let connected_sender = self.connected_sender.clone(); + let mut synced_layout = "".to_string(); + + let is_connected = Arc::new(Mutex::new(true)); + let is_connected_ref = is_connected.clone(); + std::thread::spawn(move || { + let mut connected_receiver = connected_sender.subscribe(); + loop { + if !connected_receiver.try_recv().unwrap_or(true) { + let mut is_connected = is_connected_ref.lock().unwrap(); + *is_connected = false; + break; + } + + std::thread::sleep(std::time::Duration::from_millis(100)); + }} + ); + loop { + if !*(is_connected.lock().unwrap()) { + break; + } + if let Some(layout) = get_keyboard_layout() { + let lang = layout.split('.').last().unwrap().to_string(); + if synced_layout != lang { + synced_layout = lang; + send_data(&synced_layout, &layouts, &data_sender); + } + } + unsafe {CFRunLoopRunInMode(kCFRunLoopDefaultMode, CFTimeInterval::from(1), Boolean::from(true));} + } + tracing::info!("Layout Provider stopped"); + } +}