From 7803b63a6558a341b5eac661997a4805fa59a345 Mon Sep 17 00:00:00 2001 From: Evgenii Vilkov Date: Sat, 5 Oct 2024 00:25:11 +0200 Subject: [PATCH] Add support for multiple devices in config (#8) --- README.md | 53 ++++++++---- dist/qmk-hid-host.json | 14 ++-- src/config.rs | 75 ++++++++++++----- src/keyboard.rs | 35 ++++---- src/main.rs | 138 ++++++++++++++++++-------------- src/providers/_base.rs | 1 + src/providers/layout/linux.rs | 37 +++++---- src/providers/layout/macos.rs | 62 +++++++------- src/providers/layout/windows.rs | 33 ++++---- src/providers/media/linux.rs | 32 ++++---- src/providers/media/windows.rs | 30 ++++--- src/providers/time.rs | 28 ++++--- src/providers/volume/linux.rs | 30 ++++--- src/providers/volume/macos.rs | 90 +++++++++++---------- src/providers/volume/windows.rs | 38 +++++---- 15 files changed, 403 insertions(+), 293 deletions(-) diff --git a/README.md b/README.md index f63166f..3631ab2 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ Application is written in Rust which gives easy access to HID libraries, low-lev ## Supported platforms/providers -| | Windows | Linux | macos | -| ------------ | ------------------ | ------------------------------- |--------------------| +| | Windows | Linux | MacOS | +| ------------ | ------------------ | ------------------------------- | ------------------ | | Time | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | | Volume | :heavy_check_mark: | :heavy_check_mark: (PulseAudio) | :heavy_check_mark: | | Input layout | :heavy_check_mark: | :heavy_check_mark: (X11) | :heavy_check_mark: | @@ -27,11 +27,25 @@ All files are available in [latest release](https://github.com/zzeneg/qmk-hid-ho Default configuration is set to [stront](https://github.com/zzeneg/stront). For other keyboards you need to modify the configuration file (`qmk-hid-host.json`). -- `device` section contains information about keyboard. All values are **decimal**, make sure to convert them from hex using a [converter](https://tools.keycdn.com/hex-converter). +- `devices` section contains a list of keyboards - `productId` - `pid` from your keyboard's `info.json` - - `usage` and `usagePage` - default values from QMK (`RAW_USAGE_ID` and `RAW_USAGE_PAGE`). No need to modify them unless they were redefined in firmware + - `name` - keyboard's name (optional, visible only in logs) + - `usage` and `usagePage` - optional, override only if `RAW_USAGE_ID` and `RAW_USAGE_PAGE` were redefined in firmware - `layouts` - list of supported keyboard layouts in two-letter format (app sends layout's index, not name) -- `reconnectDelay` - delay between reconnecting attempts in milliseconds +- `reconnectDelay` - delay between reconnecting attempts in milliseconds (optional, default is 5000) + +#### Minimal config + +```json +{ + "devices": [ + { + "productId": "0x0844" + } + ], + "layouts": ["en"] +} +``` Configuration is read from file `qmk-hid-host.json` in the current working directory. If it is not found, then the default configuration is written to this file. You can specify a different location for the configuration file by using `--config (-c)` command line option. For example: @@ -65,20 +79,24 @@ When you verified that the application works with your keyboard, you can use `qm 3. Start `qmk-hid-host`, add it to autorun if needed ### MacOS + 1. Download `qmk-hid-host` 2. Modify `qmk-hid-host.json` 3. Add your layouts, for example: -``` -"layouts": [ - "ABC", "Russian" -], -``` -if you don't know what layout are installed in you system, run qmk-hid-host with the layouts listed above, change lang and look at terminal output: -``` -INFO qmk_hid_host::providers::layout::macos: new layout: 'ABC', layout list: ["ABC", "Russian"] -INFO qmk_hid_host::providers::layout::macos: new layout: 'Russian', layout list: ["ABC", "Russian"] -``` -"new layout:" is what you need + + ```json + "layouts": ["ABC", "Russian"], + ``` + + if you don't know what layout are installed in you system, run qmk-hid-host with the layouts listed above, change lang and look at terminal output: + + ``` + INFO qmk_hid_host::providers::layout::macos: new layout: 'ABC', layout list: ["ABC", "Russian"] + INFO qmk_hid_host::providers::layout::macos: new layout: 'Russian', layout list: ["ABC", "Russian"] + ``` + + "new layout:" is what you need + 4. start `qmk-hid-host` from directory where your `qmk-hid-host.json` is located 5. If you `qmk-hid-host` stuck at `Waiting for keyboard...` there are two common mistakes: 1. You're wrong with productId in your config @@ -91,6 +109,9 @@ INFO qmk_hid_host::providers::layout::macos: new layout: 'Russian', layout list: 3. If needed, edit `qmk-hid-host.json` in root folder and run again ## Changelog + +- 2024-10-03 - add support for multiple devices, restructure config +- 2024-09-15 - add MacOS support - 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/dist/qmk-hid-host.json b/dist/qmk-hid-host.json index d670ce7..40d927d 100644 --- a/dist/qmk-hid-host.json +++ b/dist/qmk-hid-host.json @@ -1,9 +1,9 @@ { - "device": { - "productId": 2116, - "usage": 97, - "usagePage": 65376 - }, - "layouts": ["en"], - "reconnectDelay": 5000 + "devices": [ + { + "name": "stront", + "productId": "0x0844" + } + ], + "layouts": ["en"] } diff --git a/src/config.rs b/src/config.rs index 645479c..affeb6b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,46 +1,77 @@ -use std::path::PathBuf; +use std::{path::PathBuf, sync::OnceLock}; #[derive(serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct Config { - pub device: Device, + pub devices: Vec, pub layouts: Vec, - pub reconnect_delay: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub reconnect_delay: Option, } #[derive(serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct Device { + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(serialize_with = "hex_to_string", deserialize_with = "string_to_hex")] pub product_id: u16, - pub usage: u16, - pub usage_page: u16, + #[serde(skip_serializing_if = "Option::is_none")] + pub usage: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub usage_page: Option, } -pub fn get_config(maybe_path: Option) -> Config { +static CONFIG: OnceLock = OnceLock::new(); + +pub fn get_config() -> &'static Config { + CONFIG.get().unwrap() +} + +pub fn load_config(path: PathBuf) -> &'static Config { + if let Some(config) = CONFIG.get() { + return config; + } + let default_config = Config { - device: Device { + devices: vec![Device { + name: None, product_id: 0x0844, - usage: 0x61, - usage_page: 0xff60, - }, - layouts: vec!["pl".to_string()], - reconnect_delay: 5000, + usage: None, + usage_page: None, + }], + layouts: vec!["en".to_string()], + reconnect_delay: None, }; - let path = maybe_path.unwrap_or("./qmk-hid-host.json".into()); - if let Ok(file) = std::fs::read_to_string(&path) { - if let Ok(file_config) = serde_json::from_str::(&file) { - tracing::info!("Read config from file {:?}", path); - return file_config; - } - - tracing::error!("Error while reading config from file {:?}", path); + let config = serde_json::from_str::(&file) + .map_err(|e| tracing::error!("Incorrect config file: {}", e)) + .unwrap(); + return CONFIG.get_or_init(|| config); } let file_content = serde_json::to_string_pretty(&default_config).unwrap(); - std::fs::write(&path, &file_content).unwrap(); + std::fs::write(&path, &file_content) + .map_err(|e| tracing::error!("Error while saving config file to {:?}: {}", path, e)) + .unwrap(); tracing::info!("New config file created at {:?}", path); - return default_config; + CONFIG.get_or_init(|| default_config) +} + +fn string_to_hex<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let value: &str = serde::Deserialize::deserialize(deserializer)?; + let hex = value.trim_start_matches("0x"); + return u16::from_str_radix(hex, 16).map_err(serde::de::Error::custom); +} + +fn hex_to_string(value: &u16, serializer: S) -> Result +where + S: serde::Serializer, +{ + serializer.serialize_str(&format!("0x{:04x}", value)) } diff --git a/src/keyboard.rs b/src/keyboard.rs index f7c5226..7c6611d 100644 --- a/src/keyboard.rs +++ b/src/keyboard.rs @@ -4,6 +4,7 @@ use tokio::sync::{broadcast, mpsc}; use crate::config::Device; pub struct Keyboard { + name: String, product_id: u16, usage: u16, usage_page: u16, @@ -11,11 +12,12 @@ pub struct Keyboard { } impl Keyboard { - pub fn new(device: Device, reconnect_delay: u64) -> Self { + pub fn new(device: &Device, reconnect_delay: u64) -> Self { return Self { + name: device.name.clone().unwrap_or("keyboard".to_string()), product_id: device.product_id, - usage: device.usage, - usage_page: device.usage_page, + usage: device.usage.unwrap_or(0x61), + usage_page: device.usage_page.unwrap_or(0xff60), reconnect_delay, }; } @@ -33,30 +35,29 @@ impl Keyboard { return Err(HidError::HidApiErrorEmpty); } - pub fn connect(&self) -> (broadcast::Sender, mpsc::Sender>) { + pub fn connect(&self, data_sender: broadcast::Sender>, is_connected_sender: mpsc::Sender) { + let name = self.name.clone(); let pid = self.product_id; let usage = self.usage; let usage_page = self.usage_page; let reconnect_delay = self.reconnect_delay; - let (data_sender, mut data_receiver) = mpsc::channel::>(32); - let (connected_sender, _) = broadcast::channel::(32); - let internal_connected_sender = connected_sender.clone(); + let is_connected_sender = is_connected_sender.clone(); + let mut data_receiver = data_sender.subscribe(); std::thread::spawn(move || { - tracing::info!("Waiting for keyboard..."); + tracing::info!("Waiting for {}...", name); loop { - tracing::debug!("Trying to connect..."); + tracing::debug!("Trying to connect to {}...", name); if let Ok(device) = Self::get_device(&pid, &usage, &usage_page) { - let _ = &internal_connected_sender.send(true).unwrap(); - tracing::info!("Connected to keyboard"); + let _ = &is_connected_sender.try_send(true).unwrap_or_else(|e| tracing::error!("{}", e)); + tracing::info!("Connected to {}", name); loop { - let msg = data_receiver.blocking_recv(); - if let Some(mut received) = msg { - tracing::info!("Sending to keyboard: {:?}", received); + if let Ok(mut received) = data_receiver.blocking_recv() { + tracing::info!("Sending to {}: {:?}", name, received); received.truncate(32); received.insert(0, 0); if let Err(_) = device.write(received.as_mut()) { - let _ = internal_connected_sender.send(false).unwrap(); - tracing::warn!("Disconnected from keyboard"); + let _ = is_connected_sender.try_send(false).unwrap_or_else(|e| tracing::error!("{}", e)); + tracing::warn!("Disconnected from {}", name); break; } @@ -67,7 +68,5 @@ impl Keyboard { std::thread::sleep(std::time::Duration::from_millis(reconnect_delay)); } }); - - return (connected_sender, data_sender); } } diff --git a/src/main.rs b/src/main.rs index ad94ca4..522c7a2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,67 +8,17 @@ mod data_type; mod keyboard; mod providers; +use config::load_config; +use keyboard::Keyboard; +use providers::{_base::Provider, layout::LayoutProvider, time::TimeProvider, volume::VolumeProvider}; use std::thread; use tokio::sync::{broadcast, mpsc}; -use config::get_config; -use keyboard::Keyboard; -#[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, volume::VolumeProvider}, - core_foundation_sys::runloop::CFRunLoopRun, -}; - -#[cfg(target_os = "macos")] -fn run(layouts: Vec, data_sender: mpsc::Sender>, connected_sender: broadcast::Sender) { - let mut is_connected = false; - let mut connected_receiver = connected_sender.subscribe(); - - thread::spawn(move || { - let providers: Vec> = vec![ - TimeProvider::new(data_sender.clone(), connected_sender.clone()), - LayoutProvider::new(data_sender.clone(), connected_sender.clone(), layouts), - VolumeProvider::new(data_sender.clone(), connected_sender.clone()), - ]; - - loop { - if let Ok(connected) = connected_receiver.blocking_recv() { - if !is_connected && connected { - providers.iter().for_each(|p| p.start()); - } - - is_connected = connected; - } - } - }); - unsafe { CFRunLoopRun(); } -} #[cfg(not(target_os = "macos"))] -fn run(layouts: Vec, data_sender: mpsc::Sender>, connected_sender: broadcast::Sender) { - let providers: Vec> = vec![ - TimeProvider::new(data_sender.clone(), connected_sender.clone()), - VolumeProvider::new(data_sender.clone(), connected_sender.clone()), - LayoutProvider::new(data_sender.clone(), connected_sender.clone(), layouts), - MediaProvider::new(data_sender.clone(), connected_sender.clone()), - ]; - - let mut is_connected = false; - let mut connected_receiver = connected_sender.subscribe(); - - loop { - if let Ok(connected) = connected_receiver.blocking_recv() { - if !is_connected && connected { - providers.iter().for_each(|p| p.start()); - } +use providers::media::MediaProvider; - is_connected = connected; - } - } -} +#[cfg(target_os = "macos")] +use core_foundation_sys::runloop::CFRunLoopRun; use clap::Parser; @@ -87,10 +37,78 @@ fn main() { let _ = tracing::subscriber::set_global_default(tracing_subscriber); let args = Args::parse(); - let config = get_config(args.config); + let config = load_config(args.config.unwrap_or("./qmk-hid-host.json".into())); + + let (data_sender, _) = broadcast::channel::>(1); + let (is_connected_sender, is_connected_receiver) = mpsc::channel::(1); + + for device in &config.devices { + let data_sender = data_sender.clone(); + let is_connected_sender = is_connected_sender.clone(); + let reconnect_delay = config.reconnect_delay.unwrap_or(5000); + thread::spawn(move || { + let keyboard = Keyboard::new(device, reconnect_delay); + keyboard.connect(data_sender, is_connected_sender); + }); + } - let keyboard = Keyboard::new(config.device, config.reconnect_delay); - let (connected_sender, data_sender) = keyboard.connect(); + run(data_sender, is_connected_receiver); +} - run(config.layouts, data_sender, connected_sender); +#[cfg(not(target_os = "macos"))] +fn get_providers(data_sender: &broadcast::Sender>) -> Vec> { + return vec![ + TimeProvider::new(data_sender.clone()), + VolumeProvider::new(data_sender.clone()), + LayoutProvider::new(data_sender.clone()), + MediaProvider::new(data_sender.clone()), + ]; +} + +#[cfg(target_os = "macos")] +fn get_providers(data_sender: &broadcast::Sender>) -> Vec> { + return vec![ + TimeProvider::new(data_sender.clone()), + VolumeProvider::new(data_sender.clone()), + LayoutProvider::new(data_sender.clone()), + ]; +} + +#[cfg(not(target_os = "macos"))] +fn run(data_sender: broadcast::Sender>, is_connected_receiver: mpsc::Receiver) { + start(data_sender, is_connected_receiver); +} + +#[cfg(target_os = "macos")] +fn run(data_sender: broadcast::Sender>, mut is_connected_receiver: mpsc::Receiver) { + thread::spawn(move || { + start(data_sender, is_connected_receiver); + }); + unsafe { + CFRunLoopRun(); + } +} + +fn start(data_sender: broadcast::Sender>, mut is_connected_receiver: mpsc::Receiver) { + let providers = get_providers(&data_sender); + + let mut connected_count = 0; + let mut is_started = false; + + loop { + if let Some(is_connected) = is_connected_receiver.blocking_recv() { + connected_count += if is_connected { 1 } else { -1 }; + tracing::info!("Connected devices: {}", connected_count); + + if connected_count > 0 && !is_started { + tracing::info!("Starting providers"); + is_started = true; + providers.iter().for_each(|p| p.start()); + } else if connected_count == 0 && is_started { + tracing::info!("Stopping providers"); + is_started = false; + providers.iter().for_each(|p| p.stop()); + } + } + } } diff --git a/src/providers/_base.rs b/src/providers/_base.rs index 7db5903..b4ffd1d 100644 --- a/src/providers/_base.rs +++ b/src/providers/_base.rs @@ -1,3 +1,4 @@ pub trait Provider { fn start(&self); + fn stop(&self); } diff --git a/src/providers/layout/linux.rs b/src/providers/layout/linux.rs index 5ab92a2..b9251a7 100644 --- a/src/providers/layout/linux.rs +++ b/src/providers/layout/linux.rs @@ -1,8 +1,11 @@ +use std::sync::atomic::{AtomicBool, Ordering::Relaxed}; +use std::sync::Arc; use std::{ffi, mem, ptr}; +use tokio::sync::broadcast; +use x11::xlib::{XGetAtomName, XOpenDisplay, XkbAllocKeyboard, XkbGetNames, XkbGetState, _XDisplay, _XkbDesc, _XkbStateRec}; +use crate::config::get_config; use crate::data_type::DataType; -use tokio::sync::{broadcast, mpsc}; -use x11::xlib::{XGetAtomName, XOpenDisplay, XkbAllocKeyboard, XkbGetNames, XkbGetState, _XDisplay, _XkbDesc, _XkbStateRec}; use super::super::_base::Provider; @@ -24,27 +27,25 @@ fn get_layout_index(display: *mut _XDisplay) -> usize { return state.group as usize; } -fn send_data(value: &String, layouts: &Vec, data_sender: &mpsc::Sender>) { +fn send_data(value: &String, layouts: &Vec, data_sender: &broadcast::Sender>) { tracing::info!("new layout: '{0}', layout list: {1:?}", value, layouts); let index = layouts.into_iter().position(|r| r == value); if let Some(index) = index { let data = vec![DataType::Layout as u8, index as u8]; - data_sender.try_send(data).unwrap_or_else(|e| tracing::error!("{}", e)); + data_sender.send(data).unwrap(); } } pub struct LayoutProvider { - data_sender: mpsc::Sender>, - connected_sender: broadcast::Sender, - layouts: Vec, + data_sender: broadcast::Sender>, + is_started: Arc, } impl LayoutProvider { - pub fn new(data_sender: mpsc::Sender>, connected_sender: broadcast::Sender, layouts: Vec) -> Box { + pub fn new(data_sender: broadcast::Sender>) -> Box { let provider = LayoutProvider { data_sender, - connected_sender, - layouts, + is_started: Arc::new(AtomicBool::new(false)), }; return Box::new(provider); } @@ -53,13 +54,11 @@ impl LayoutProvider { impl Provider for LayoutProvider { fn start(&self) { tracing::info!("Layout Provider started"); - + self.is_started.store(true, Relaxed); + let layouts = &get_config().layouts; let data_sender = self.data_sender.clone(); - let connected_sender = self.connected_sender.clone(); - let layouts = self.layouts.clone(); - + let is_started = self.is_started.clone(); std::thread::spawn(move || { - let mut connected_receiver = connected_sender.subscribe(); let mut synced_layout = 0; let display = unsafe { XOpenDisplay(ptr::null()) }; let keyboard = unsafe { XkbAllocKeyboard() }; @@ -67,7 +66,7 @@ impl Provider for LayoutProvider { let symbol_list = symbols.split('+').map(|x| x.to_string()).collect::>(); loop { - if !connected_receiver.try_recv().unwrap_or(true) { + if !is_started.load(Relaxed) { break; } @@ -76,7 +75,7 @@ impl Provider for LayoutProvider { synced_layout = layout; let layout_symbol = symbol_list.get(layout + 1).map(|x| x.to_string()).unwrap_or_default(); let layout_name = layout_symbol.split([':', '(']).next().unwrap_or_default().to_string(); - send_data(&layout_name, &layouts, &data_sender); + send_data(&layout_name, layouts, &data_sender); } std::thread::sleep(std::time::Duration::from_millis(100)); @@ -85,4 +84,8 @@ impl Provider for LayoutProvider { tracing::info!("Layout Provider stopped"); }); } + + fn stop(&self) { + self.is_started.store(false, Relaxed); + } } diff --git a/src/providers/layout/macos.rs b/src/providers/layout/macos.rs index 67b0909..8656a7d 100644 --- a/src/providers/layout/macos.rs +++ b/src/providers/layout/macos.rs @@ -1,8 +1,13 @@ -use crate::data_type::DataType; use core_foundation::base::{CFRelease, TCFType}; use core_foundation::string::{CFString, CFStringRef}; use libc::c_void; -use tokio::sync::{broadcast, mpsc}; +use std::sync::atomic::{AtomicBool, Ordering::Relaxed}; +use std::sync::Arc; +use tokio::sync::broadcast; + +use crate::config::get_config; +use crate::data_type::DataType; + use super::super::_base::Provider; #[link(name = "Carbon", kind = "framework")] @@ -13,7 +18,6 @@ extern "C" { fn get_keyboard_layout() -> Option { unsafe { - let layout_input_source = TISCopyCurrentKeyboardLayoutInputSource(); if layout_input_source.is_null() { return None; @@ -39,26 +43,24 @@ fn get_keyboard_layout() -> Option { } } -fn send_data(value: &String, layouts: &Vec, data_sender: &mpsc::Sender>) { +fn send_data(value: &String, layouts: &Vec, data_sender: &broadcast::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)); + data_sender.send(data).unwrap(); } } pub struct LayoutProvider { - data_sender: mpsc::Sender>, - connected_sender: broadcast::Sender, - layouts: Vec, + data_sender: broadcast::Sender>, + is_started: Arc, } impl LayoutProvider { - pub fn new(data_sender: mpsc::Sender>, connected_sender: broadcast::Sender, layouts: Vec) -> Box { + pub fn new(data_sender: broadcast::Sender>) -> Box { let provider = LayoutProvider { data_sender, - connected_sender, - layouts, + is_started: Arc::new(AtomicBool::new(false)), }; Box::new(provider) } @@ -67,29 +69,31 @@ impl LayoutProvider { impl Provider for LayoutProvider { fn start(&self) { tracing::info!("Layout Provider started"); - + self.is_started.store(true, Relaxed); + let layouts = &get_config().layouts; let data_sender = self.data_sender.clone(); - let layouts = self.layouts.clone(); - let connected_sender = self.connected_sender.clone(); + let is_started = self.is_started.clone(); let mut synced_layout = "".to_string(); - std::thread::spawn(move || { - let mut connected_receiver = connected_sender.subscribe(); - loop { - if !connected_receiver.try_recv().unwrap_or(true) { - 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); - } + std::thread::spawn(move || loop { + if !is_started.load(Relaxed) { + 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); } - std::thread::sleep(std::time::Duration::from_millis(100)); - }} - ); + } + std::thread::sleep(std::time::Duration::from_millis(100)); + }); tracing::info!("Layout Provider stopped"); } + + fn stop(&self) { + self.is_started.store(false, Relaxed); + } } diff --git a/src/providers/layout/windows.rs b/src/providers/layout/windows.rs index 4b4bf85..9156620 100644 --- a/src/providers/layout/windows.rs +++ b/src/providers/layout/windows.rs @@ -1,4 +1,6 @@ -use tokio::sync::{broadcast, mpsc}; +use std::sync::atomic::{AtomicBool, Ordering::Relaxed}; +use std::sync::Arc; +use tokio::sync::broadcast; use windows::Win32::{ Globalization::{GetLocaleInfoW, LOCALE_SISO639LANGNAME}, UI::{ @@ -8,6 +10,7 @@ use windows::Win32::{ }, }; +use crate::config::get_config; use crate::data_type::DataType; use super::super::_base::Provider; @@ -26,25 +29,23 @@ unsafe fn get_layout() -> Option { None } -fn send_data(value: &String, layouts: &Vec, data_sender: &mpsc::Sender>) { +fn send_data(value: &String, layouts: &Vec, data_sender: &broadcast::Sender>) { 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)); + data_sender.send(data).unwrap(); } } pub struct LayoutProvider { - data_sender: mpsc::Sender>, - connected_sender: broadcast::Sender, - layouts: Vec, + data_sender: broadcast::Sender>, + is_started: Arc, } impl LayoutProvider { - pub fn new(data_sender: mpsc::Sender>, connected_sender: broadcast::Sender, layouts: Vec) -> Box { + pub fn new(data_sender: broadcast::Sender>) -> Box { let provider = LayoutProvider { data_sender, - connected_sender, - layouts, + is_started: Arc::new(AtomicBool::new(false)), }; return Box::new(provider); } @@ -53,21 +54,21 @@ impl LayoutProvider { impl Provider for LayoutProvider { fn start(&self) { tracing::info!("Layout Provider started"); + self.is_started.store(true, Relaxed); + let layouts = &get_config().layouts; let data_sender = self.data_sender.clone(); - let connected_sender = self.connected_sender.clone(); - let layouts = self.layouts.clone(); + let is_started = self.is_started.clone(); std::thread::spawn(move || { - let mut connected_receiver = connected_sender.subscribe(); let mut synced_layout = "".to_string(); loop { - if !connected_receiver.try_recv().unwrap_or(true) { + if !is_started.load(Relaxed) { break; } if let Some(layout) = unsafe { get_layout() } { if synced_layout != layout { synced_layout = layout; - send_data(&synced_layout, &layouts, &data_sender); + send_data(&synced_layout, layouts, &data_sender); } } @@ -77,4 +78,8 @@ impl Provider for LayoutProvider { tracing::info!("Layout Provider stopped"); }); } + + fn stop(&self) { + self.is_started.store(false, Relaxed); + } } diff --git a/src/providers/media/linux.rs b/src/providers/media/linux.rs index d18091b..266ae7d 100644 --- a/src/providers/media/linux.rs +++ b/src/providers/media/linux.rs @@ -1,11 +1,13 @@ use mpris::{Metadata, PlayerFinder}; -use tokio::sync::{broadcast, mpsc}; +use std::sync::atomic::{AtomicBool, Ordering::Relaxed}; +use std::sync::Arc; +use tokio::sync::broadcast; use crate::data_type::DataType; use super::super::_base::Provider; -fn send_media_data(metadata: &Metadata, data_sender: &mpsc::Sender>, current: &(String, String)) -> (String, String) { +fn send_media_data(metadata: &Metadata, data_sender: &broadcast::Sender>, current: &(String, String)) -> (String, String) { let (mut artist, mut title) = current.clone(); let new_artist = metadata.artists().and_then(|x| x.get(0).map(|x| x.to_string())).unwrap_or_default(); @@ -26,24 +28,24 @@ fn send_media_data(metadata: &Metadata, data_sender: &mpsc::Sender>, cur return (artist, title); } -fn send_data(data_type: DataType, value: &String, data_sender: &mpsc::Sender>) { +fn send_data(data_type: DataType, value: &String, data_sender: &broadcast::Sender>) { let mut data = value.to_string().into_bytes(); data.truncate(30); data.insert(0, data.len() as u8); data.insert(0, data_type as u8); - data_sender.try_send(data).unwrap_or_else(|e| tracing::error!("{}", e)); + data_sender.send(data).unwrap(); } pub struct MediaProvider { - data_sender: mpsc::Sender>, - connected_sender: broadcast::Sender, + data_sender: broadcast::Sender>, + is_started: Arc, } impl MediaProvider { - pub fn new(data_sender: mpsc::Sender>, connected_sender: broadcast::Sender) -> Box { + pub fn new(data_sender: broadcast::Sender>) -> Box { let provider = MediaProvider { data_sender, - connected_sender, + is_started: Arc::new(AtomicBool::new(false)), }; return Box::new(provider); } @@ -52,16 +54,14 @@ impl MediaProvider { impl Provider for MediaProvider { fn start(&self) { tracing::info!("Media Provider started"); - + self.is_started.store(true, Relaxed); let data_sender = self.data_sender.clone(); - let connected_sender = self.connected_sender.clone(); + let is_started = self.is_started.clone(); std::thread::spawn(move || { - let mut connected_receiver = connected_sender.subscribe(); - let mut media_data = (String::default(), String::default()); 'outer: loop { - if !connected_receiver.try_recv().unwrap_or(true) { + if !is_started.load(Relaxed) { break; } @@ -74,7 +74,7 @@ impl Provider for MediaProvider { for event in events { tracing::debug!("{:?}", event); - if !connected_receiver.try_recv().unwrap_or(true) { + if !is_started.load(Relaxed) { break 'outer; } @@ -101,4 +101,8 @@ impl Provider for MediaProvider { tracing::info!("Media Provider stopped"); }); } + + fn stop(&self) { + self.is_started.store(false, Relaxed); + } } diff --git a/src/providers/media/windows.rs b/src/providers/media/windows.rs index 29a3f3c..37315c1 100644 --- a/src/providers/media/windows.rs +++ b/src/providers/media/windows.rs @@ -1,5 +1,6 @@ -use tokio::sync::{broadcast, mpsc}; - +use std::sync::atomic::{AtomicBool, Ordering::Relaxed}; +use std::sync::Arc; +use tokio::sync::broadcast; use windows::{ Foundation::{EventRegistrationToken, TypedEventHandler}, Media::Control::{GlobalSystemMediaTransportControlsSession, GlobalSystemMediaTransportControlsSessionManager}, @@ -17,7 +18,7 @@ fn get_manager() -> Result fn handle_session( session: &GlobalSystemMediaTransportControlsSession, - data_sender: &mpsc::Sender>, + data_sender: &broadcast::Sender>, ) -> Option { let mut synced_artist = String::new(); let mut synced_title = String::new(); @@ -68,24 +69,24 @@ fn get_media_data(session: &GlobalSystemMediaTransportControlsSession) -> Option None } -fn send_data(data_type: DataType, value: &String, data_sender: &mpsc::Sender>) { +fn send_data(data_type: DataType, value: &String, data_sender: &broadcast::Sender>) { let mut data = value.to_string().into_bytes(); data.truncate(30); data.insert(0, data.len() as u8); data.insert(0, data_type as u8); - data_sender.try_send(data).unwrap_or_else(|e| tracing::error!("{}", e)); + data_sender.send(data).unwrap(); } pub struct MediaProvider { - data_sender: mpsc::Sender>, - connected_sender: broadcast::Sender, + data_sender: broadcast::Sender>, + is_started: Arc, } impl MediaProvider { - pub fn new(data_sender: mpsc::Sender>, connected_sender: broadcast::Sender) -> Box { + pub fn new(data_sender: broadcast::Sender>) -> Box { let provider = MediaProvider { data_sender, - connected_sender, + is_started: Arc::new(AtomicBool::new(false)), }; return Box::new(provider); } @@ -94,11 +95,10 @@ impl MediaProvider { impl Provider for MediaProvider { fn start(&self) { tracing::info!("Media Provider started"); - + self.is_started.store(true, Relaxed); let data_sender = self.data_sender.clone(); - let connected_sender = self.connected_sender.clone(); + let is_started = self.is_started.clone(); std::thread::spawn(move || { - let mut connected_receiver = connected_sender.subscribe(); let mut session_token: Option = None; if let Ok(manager) = get_manager() { @@ -123,7 +123,7 @@ impl Provider for MediaProvider { .map_err(|e| tracing::error!("Can not register CurrentSessionChanged callback: {}", e)); loop { - if !connected_receiver.try_recv().unwrap_or(true) { + if !is_started.load(Relaxed) { break; } @@ -138,4 +138,8 @@ impl Provider for MediaProvider { } }); } + + fn stop(&self) { + self.is_started.store(false, Relaxed); + } } diff --git a/src/providers/time.rs b/src/providers/time.rs index 6257420..8057f41 100644 --- a/src/providers/time.rs +++ b/src/providers/time.rs @@ -1,5 +1,7 @@ use chrono::{DateTime, Local, Timelike}; -use tokio::sync::{broadcast, mpsc}; +use std::sync::atomic::{AtomicBool, Ordering::Relaxed}; +use std::sync::Arc; +use tokio::sync::broadcast; use crate::data_type::DataType; @@ -12,21 +14,21 @@ fn get_time() -> (u8, u8) { return (hour, minute); } -fn send_data(value: &(u8, u8), push_sender: &mpsc::Sender>) { +fn send_data(value: &(u8, u8), push_sender: &broadcast::Sender>) { let data = vec![DataType::Time as u8, value.0, value.1]; - push_sender.try_send(data).unwrap_or_else(|e| tracing::error!("{}", e)); + push_sender.send(data).unwrap(); } pub struct TimeProvider { - data_sender: mpsc::Sender>, - connected_sender: broadcast::Sender, + data_sender: broadcast::Sender>, + is_started: Arc, } impl TimeProvider { - pub fn new(data_sender: mpsc::Sender>, connected_sender: broadcast::Sender) -> Box { + pub fn new(data_sender: broadcast::Sender>) -> Box { let provider = TimeProvider { data_sender, - connected_sender, + is_started: Arc::new(AtomicBool::new(false)), }; return Box::new(provider); } @@ -34,14 +36,14 @@ impl TimeProvider { impl Provider for TimeProvider { fn start(&self) { - tracing::info!("Time Provider enabled"); + tracing::info!("Time Provider started"); + self.is_started.store(true, Relaxed); let data_sender = self.data_sender.clone(); - let connected_sender = self.connected_sender.clone(); + let is_started = self.is_started.clone(); std::thread::spawn(move || { - let mut connected_receiver = connected_sender.subscribe(); let mut synced_time = (0u8, 0u8); loop { - if !connected_receiver.try_recv().unwrap_or(true) { + if !is_started.load(Relaxed) { break; } @@ -57,4 +59,8 @@ impl Provider for TimeProvider { tracing::info!("Time Provider stopped"); }); } + + fn stop(&self) { + self.is_started.store(false, Relaxed); + } } diff --git a/src/providers/volume/linux.rs b/src/providers/volume/linux.rs index 76c09db..a5b71f4 100644 --- a/src/providers/volume/linux.rs +++ b/src/providers/volume/linux.rs @@ -1,8 +1,9 @@ -use std::ops::Deref; - use libpulse_binding::context::subscribe::Facility; use pulsectl::controllers::{DeviceControl, SinkController}; -use tokio::sync::{broadcast, mpsc}; +use std::ops::Deref; +use std::sync::atomic::{AtomicBool, Ordering::Relaxed}; +use std::sync::Arc; +use tokio::sync::broadcast; use crate::data_type::DataType; @@ -19,22 +20,22 @@ fn get_volume() -> Option { return None; } -fn send_data(value: &f32, push_sender: &mpsc::Sender>) { +fn send_data(value: &f32, push_sender: &broadcast::Sender>) { let volume = (value * 100.0).round() as u8; let data = vec![DataType::Volume as u8, volume]; - push_sender.try_send(data).unwrap_or_else(|e| tracing::error!("{}", e)); + push_sender.send(data).unwrap(); } pub struct VolumeProvider { - data_sender: mpsc::Sender>, - connected_sender: broadcast::Sender, + data_sender: broadcast::Sender>, + is_started: Arc, } impl VolumeProvider { - pub fn new(data_sender: mpsc::Sender>, connected_sender: broadcast::Sender) -> Box { + pub fn new(data_sender: broadcast::Sender>) -> Box { let provider = VolumeProvider { data_sender, - connected_sender, + is_started: Arc::new(AtomicBool::new(false)), }; return Box::new(provider); } @@ -43,15 +44,14 @@ impl VolumeProvider { impl Provider for VolumeProvider { fn start(&self) { tracing::info!("Volume Provider started"); + self.is_started.store(true, Relaxed); let data_sender = self.data_sender.clone(); - let connected_sender = self.connected_sender.clone(); + let is_started = self.is_started.clone(); let mut volume = get_volume().unwrap_or_default(); send_data(&volume, &self.data_sender); std::thread::spawn(move || { - let mut connected_receiver = connected_sender.subscribe(); - let controller = SinkController::create().map_err(|e| tracing::error!("{}", e)).unwrap(); let mut ctx = controller.handler.context.deref().borrow_mut(); @@ -66,7 +66,7 @@ impl Provider for VolumeProvider { ctx.subscribe(Facility::Sink.to_interest_mask(), |_| {}); loop { - if !connected_receiver.try_recv().unwrap_or(true) { + if !is_started.load(Relaxed) { break; } @@ -76,4 +76,8 @@ impl Provider for VolumeProvider { tracing::info!("Volume Provider stopped"); }); } + + fn stop(&self) { + self.is_started.store(false, Relaxed); + } } diff --git a/src/providers/volume/macos.rs b/src/providers/volume/macos.rs index d87026a..e3e7ab4 100644 --- a/src/providers/volume/macos.rs +++ b/src/providers/volume/macos.rs @@ -1,13 +1,18 @@ -use crate::providers::_base::Provider; use block2::{Block, RcBlock}; use coreaudio::audio_unit::macos_helpers::get_default_device_id; -use coreaudio_sys::{dispatch_queue_t, kAudioDevicePropertyScopeOutput, kAudioDevicePropertyVolumeScalar, kAudioHardwarePropertyDefaultOutputDevice, kAudioObjectPropertyElementMain, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyScopeOutput, kAudioObjectSystemObject, AudioObjectGetPropertyData, AudioObjectID, AudioObjectIsPropertySettable, AudioObjectPropertyAddress, OSStatus}; +use coreaudio_sys::{ + dispatch_queue_t, kAudioDevicePropertyScopeOutput, kAudioDevicePropertyVolumeScalar, kAudioHardwarePropertyDefaultOutputDevice, + kAudioObjectPropertyElementMain, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyScopeOutput, kAudioObjectSystemObject, + AudioObjectGetPropertyData, AudioObjectID, AudioObjectIsPropertySettable, AudioObjectPropertyAddress, OSStatus, +}; use std::option::Option; use std::ptr; -use tokio::sync::{broadcast, mpsc}; +use std::sync::atomic::{AtomicBool, Ordering::Relaxed}; +use std::sync::Arc; +use tokio::sync::broadcast; use crate::data_type::DataType; - +use crate::providers::_base::Provider; extern "C" { pub fn AudioObjectAddPropertyListenerBlock( @@ -68,13 +73,7 @@ fn is_volume_control_supported(device_id: AudioObjectID, channel: u32) -> bool { mElement: channel, }; - let status = unsafe { - AudioObjectIsPropertySettable( - device_id, - &property_address, - &mut is_writable, - ) - }; + let status = unsafe { AudioObjectIsPropertySettable(device_id, &property_address, &mut is_writable) }; status == 0 && is_writable != 0 } @@ -104,31 +103,39 @@ fn register_volume_listener(listener: &RcBlock) { mElement: channel.unwrap(), }; - let listener_status = unsafe { AudioObjectRemovePropertyListenerBlock(device_id.unwrap(), &property_address, ptr::null_mut(), &listener)}; + let listener_status = + unsafe { AudioObjectRemovePropertyListenerBlock(device_id.unwrap(), &property_address, ptr::null_mut(), &listener) }; if listener_status == 0 { tracing::info!( "Volume listener successfully removed for channel {} of device {}", - channel.unwrap(), device_id.unwrap() + channel.unwrap(), + device_id.unwrap() ); } else { - tracing::info!("Failed to remove volume listener for channel {} of device {}", channel.unwrap(), device_id.unwrap()) + tracing::info!( + "Failed to remove volume listener for channel {} of device {}", + channel.unwrap(), + device_id.unwrap() + ) } - let listener_status = unsafe { - AudioObjectAddPropertyListenerBlock(device_id.unwrap(), &property_address, ptr::null_mut(), &listener) - }; + let listener_status = unsafe { AudioObjectAddPropertyListenerBlock(device_id.unwrap(), &property_address, ptr::null_mut(), &listener) }; if listener_status == 0 { tracing::info!( "Volume listener successfully registered for channel {} of device {}", - channel.unwrap(), device_id.unwrap() + channel.unwrap(), + device_id.unwrap() ); } else { - tracing::info!("Failed to register volume listener for channel {} of device {}", channel.unwrap(), device_id.unwrap()) + tracing::info!( + "Failed to register volume listener for channel {} of device {}", + channel.unwrap(), + device_id.unwrap() + ) } } - fn register_device_change_listener(listener: &RcBlock) { let property_address = AudioObjectPropertyAddress { mSelector: kAudioHardwarePropertyDefaultOutputDevice, @@ -136,18 +143,16 @@ fn register_device_change_listener(listener: &RcBlock) { mElement: kAudioObjectPropertyElementMain, }; - let listener_status = unsafe { - AudioObjectRemovePropertyListenerBlock(kAudioObjectSystemObject, &property_address, ptr::null_mut(), &listener) - }; + let listener_status = + unsafe { AudioObjectRemovePropertyListenerBlock(kAudioObjectSystemObject, &property_address, ptr::null_mut(), &listener) }; if listener_status == 0 { tracing::info!("Default device change listener successfully removed"); } else { tracing::info!("Failed to remove default device change listener"); } - let listener_status = unsafe { - AudioObjectAddPropertyListenerBlock(kAudioObjectSystemObject, &property_address, ptr::null_mut(), &listener) - }; + let listener_status = + unsafe { AudioObjectAddPropertyListenerBlock(kAudioObjectSystemObject, &property_address, ptr::null_mut(), &listener) }; if listener_status == 0 { tracing::info!("Default device change listener registered successfully"); @@ -156,60 +161,57 @@ fn register_device_change_listener(listener: &RcBlock) { } } -fn send_data(value: &f32, push_sender: &mpsc::Sender>) { +fn send_data(value: &f32, push_sender: &broadcast::Sender>) { let volume = (value * 100.0).round() as u8; let data = vec![DataType::Volume as u8, volume]; - push_sender.try_send(data).unwrap_or_else(|e| tracing::error!("{}", e)); + push_sender.send(data).unwrap(); } pub struct VolumeProvider { - connected_sender: broadcast::Sender, + is_started: Arc, device_changed_block: RcBlock, volume_changed_block: RcBlock, } impl VolumeProvider { - pub fn new(data_sender: mpsc::Sender>, connected_sender: broadcast::Sender) -> Box { + pub fn new(data_sender: broadcast::Sender>) -> Box { let sender = data_sender.clone(); - let volume_changed_block = RcBlock::new(move |_: u32, _: u64|{ - if let Some(volume) = get_current_volume(){ + let volume_changed_block = RcBlock::new(move |_: u32, _: u64| { + if let Some(volume) = get_current_volume() { send_data(&volume, &sender.clone()); } }); let sender = data_sender.clone(); let volume_changed_block_clone = volume_changed_block.clone(); - let device_changed_block: RcBlock = RcBlock::new(move |_: u32, _: u64|{ + let device_changed_block: RcBlock = RcBlock::new(move |_: u32, _: u64| { register_volume_listener(&volume_changed_block_clone); - if let Some(volume) = get_current_volume(){ + if let Some(volume) = get_current_volume() { send_data(&volume, &sender.clone()); } }); let provider = VolumeProvider { - connected_sender, + is_started: Arc::new(AtomicBool::new(false)), device_changed_block, volume_changed_block, }; Box::new(provider) } - } impl Provider for VolumeProvider { - fn start(&self) { tracing::info!("Volume Provider started"); - let connected_sender = self.connected_sender.clone(); + self.is_started.store(true, Relaxed); + let is_started = self.is_started.clone(); register_volume_listener(&self.volume_changed_block); register_device_change_listener(&self.device_changed_block); std::thread::spawn(move || { - let mut connected_receiver = connected_sender.subscribe(); - loop { - if !connected_receiver.try_recv().unwrap_or(true) { + if !is_started.load(Relaxed) { break; } @@ -219,4 +221,8 @@ impl Provider for VolumeProvider { tracing::info!("Volume Provider stopped"); }); } -} \ No newline at end of file + + fn stop(&self) { + self.is_started.store(false, Relaxed); + } +} diff --git a/src/providers/volume/windows.rs b/src/providers/volume/windows.rs index b37b55d..42761df 100644 --- a/src/providers/volume/windows.rs +++ b/src/providers/volume/windows.rs @@ -1,7 +1,6 @@ -use tokio::sync::{ - broadcast::{self, Receiver}, - mpsc::{self, Sender}, -}; +use std::sync::atomic::{AtomicBool, Ordering::Relaxed}; +use std::sync::Arc; +use tokio::sync::broadcast; use windows::{ core::Error, Win32::{ @@ -33,7 +32,7 @@ unsafe fn get_volume_endpoint() -> Result { #[windows::core::implement(IAudioEndpointVolumeCallback)] struct VolumeChangeCallback { - push_sender: mpsc::Sender>, + push_sender: broadcast::Sender>, } impl IAudioEndpointVolumeCallback_Impl for VolumeChangeCallback { @@ -44,22 +43,22 @@ impl IAudioEndpointVolumeCallback_Impl for VolumeChangeCallback { } } -fn send_data(value: &f32, push_sender: &mpsc::Sender>) { +fn send_data(value: &f32, push_sender: &broadcast::Sender>) { let volume = (value * 100.0).round() as u8; let data = vec![DataType::Volume as u8, volume]; - push_sender.try_send(data).unwrap_or_else(|e| tracing::error!("{}", e)); + push_sender.send(data).unwrap(); } pub struct VolumeProvider { - data_sender: mpsc::Sender>, - connected_sender: broadcast::Sender, + data_sender: broadcast::Sender>, + is_started: Arc, } impl VolumeProvider { - pub fn new(data_sender: mpsc::Sender>, connected_sender: broadcast::Sender) -> Box { + pub fn new(data_sender: broadcast::Sender>) -> Box { let provider = VolumeProvider { data_sender, - connected_sender, + is_started: Arc::new(AtomicBool::new(false)), }; return Box::new(provider); } @@ -68,15 +67,15 @@ impl VolumeProvider { impl Provider for VolumeProvider { fn start(&self) { tracing::info!("Volume Provider started"); + self.is_started.store(true, Relaxed); if let Ok(volume) = get_volume() { send_data(&volume, &self.data_sender); } let data_sender = self.data_sender.clone(); - let connected_sender = self.connected_sender.clone(); + let is_started = self.is_started.clone(); std::thread::spawn(move || loop { - let connected_receiver = connected_sender.subscribe(); - if subscribe_and_wait(data_sender.clone(), connected_receiver) { + if subscribe_and_wait(&data_sender, &is_started) { tracing::info!("Volume Provider stopped"); break; } @@ -84,18 +83,23 @@ impl Provider for VolumeProvider { std::thread::sleep(std::time::Duration::from_millis(10000)); }); } + + fn stop(&self) { + self.is_started.store(false, Relaxed); + } } -fn subscribe_and_wait(data_sender: Sender>, mut connected_receiver: Receiver) -> bool { +fn subscribe_and_wait(data_sender: &broadcast::Sender>, is_started: &Arc) -> bool { if let Ok(endpoint_volume) = unsafe { get_volume_endpoint() } { - let volume_callback: IAudioEndpointVolumeCallback = VolumeChangeCallback { push_sender: data_sender }.into(); + let push_sender = data_sender.clone(); + let volume_callback: IAudioEndpointVolumeCallback = VolumeChangeCallback { push_sender }.into(); if let Err(e) = unsafe { endpoint_volume.RegisterControlChangeNotify(&volume_callback) } { tracing::error!("Can not register Volume callback: {}", e); return false; } loop { - if !connected_receiver.try_recv().unwrap_or(true) { + if !is_started.load(Relaxed) { break; }