-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Loading status checks…
WIP: feat(eww): Add bluetooth indicator
Showing
8 changed files
with
1,627 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,10 @@ | ||
;;; Directory Local Variables -*- no-byte-compile: t -*- | ||
;;; For more information see (info "(emacs) Directory Variables") | ||
|
||
((emacs-lisp-mode . ((fill-column . 88) | ||
(indent-tabs-mode . nil) | ||
(elisp-lint-indent-specs . ((use-package . 1) | ||
(reformatter-define . 1))) | ||
))) | ||
(reformatter-define . 1))))) | ||
(rust-mode | ||
. ((eglot-workspace-configuration | ||
. (:rust-analyzer (:linkedProjects ["./home-config/dotfiles/eww/utils/Cargo.toml"])))))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
target/ |
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
[package] | ||
name = "utils" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
[dependencies] | ||
futures = "0.3.31" | ||
lazy-regex = "3.4.1" | ||
log = { version = "0.4.22", features = ["kv", "kv_std"] } | ||
pretty_env_logger = "0.5.0" | ||
serde = { version = "1.0.217", features = ["serde_derive"] } | ||
serde_json = "1.0.134" | ||
thiserror = "2.0.9" | ||
tokio = { version = "1.42.0", features = ["macros"] } | ||
zbus = { version = "5.2.0", features = ["tokio"] } |
16 changes: 16 additions & 0 deletions
16
home-config/dotfiles/eww/utils/src/bin/bluetooth-monitor.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
use futures::StreamExt; | ||
use log::LevelFilter; | ||
use utils::bluetooth_monitor::BluetoothMonitor; | ||
|
||
#[tokio::main(flavor = "current_thread")] | ||
async fn main() { | ||
pretty_env_logger::formatted_builder() | ||
.filter_level(LevelFilter::Info) | ||
.init(); | ||
|
||
let monitor = BluetoothMonitor::try_new().await.unwrap(); | ||
let mut stream = monitor.monitor().await; | ||
while let Some(event) = stream.next().await { | ||
println!("{:?}", event); | ||
} | ||
} |
153 changes: 153 additions & 0 deletions
153
home-config/dotfiles/eww/utils/src/bluetooth_monitor.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
use std::collections::HashMap; | ||
|
||
/// Bluetooth device/status monitoring. | ||
use futures::{stream, Stream}; | ||
use log::error; | ||
use zbus::{fdo::ObjectManagerProxy, proxy, Connection}; | ||
|
||
use crate::MacAddress; | ||
|
||
/// Enum to hold the current Bluetooth state | ||
#[derive(Debug)] | ||
pub enum BluetoothState { | ||
NoService, | ||
NoAdapter, | ||
ConnectedDevices(Vec<MacAddress>), | ||
} | ||
|
||
/// Simple handler for tracking BlueZ state | ||
pub struct BluetoothMonitor<'a> { | ||
object_manager: ObjectManagerProxy<'a>, | ||
} | ||
|
||
impl<'a> BluetoothMonitor<'a> { | ||
/// Basic constructor | ||
pub async fn try_new() -> Result<Self> { | ||
let connection = Connection::system().await?; | ||
|
||
let object_manager = ObjectManagerProxy::builder(&connection) | ||
.path("/")? | ||
.destination("org.bluez")? | ||
.build() | ||
.await?; | ||
|
||
Ok(Self { object_manager }) | ||
} | ||
|
||
/// Monitor the Bluetooth state, emitting the current state | ||
/// whenever something changes | ||
pub async fn monitor(&self) -> impl Stream<Item = BluetoothState> + use<'_> { | ||
Box::pin(stream::once(async { self.get_state_proactively().await })) | ||
} | ||
|
||
/// Get the current Bluetooth state by explicitly calling methods | ||
/// to find out what BlueZ is currently managing. | ||
async fn get_state_proactively(&self) -> BluetoothState { | ||
match self.object_manager.get_managed_objects().await { | ||
Ok(objects) => { | ||
if !objects | ||
.iter() | ||
.any(|(_, object)| object.contains_key("org.bluez.Adapter1")) | ||
{ | ||
return BluetoothState::NoAdapter; | ||
} | ||
|
||
let devices: Vec<MacAddress> = objects | ||
.into_iter() | ||
.filter_map(|(path, object)| { | ||
object | ||
.get("org.bluez.Device1") | ||
.map(|device| (path, device.clone())) | ||
}) | ||
.filter(is_device_connected) | ||
.filter_map(get_device_address) | ||
.collect(); | ||
|
||
BluetoothState::ConnectedDevices(devices) | ||
} | ||
Err(error) => { | ||
error!(error:?; "Error connecting to BlueZ: {}", error); | ||
BluetoothState::NoService | ||
} | ||
} | ||
} | ||
|
||
// /// Monitor connections to the given MAC addresses | ||
// // TODO: This should probably be a stream of state changes, which | ||
// // the front-end can then turn into pretty JSON messages to be | ||
// // read by eww | ||
// async fn monitor_state(&self, adapter: OwnedObjectPath) -> impl Stream<Item = BluetoothState> { | ||
// todo!() | ||
// // let proxy = BluezDeviceProxy::builder(&self.connection) | ||
// // .path(format!("{adapter}/dev_80_C3_BA_6A_26_88"))? | ||
// // .build() | ||
// // .await?; | ||
|
||
// // let mut stream = proxy.receive_connected_changed().await; | ||
|
||
// // while let Some(change) = stream.next().await { | ||
// // println!("{:?}", change.get().await); | ||
// // } | ||
|
||
// // todo!() | ||
// } | ||
} | ||
|
||
/// BlueZ device interface | ||
#[proxy(default_service = "org.bluez", interface = "org.bluez.Device1")] | ||
trait BluezDevice { | ||
#[zbus(property)] | ||
fn connected(&self) -> zbus::Result<bool>; | ||
} | ||
|
||
/// The way zbus reports Bluetooth devices | ||
type BluetoothDevice = ( | ||
zbus::zvariant::OwnedObjectPath, | ||
HashMap<String, zbus::zvariant::OwnedValue>, | ||
); | ||
|
||
/// Get the address of a device | ||
fn get_device_address(device: BluetoothDevice) -> Option<MacAddress> { | ||
let address = device.1.get("Address")?; | ||
|
||
let Ok(address) = <&str>::try_from(address) else { | ||
error!( | ||
"Invalid MAC address for device Symbol’s value as variable is void: {}: {:?}", | ||
device.0, address | ||
); | ||
return None; | ||
}; | ||
|
||
let Ok(address) = MacAddress::try_from(address.to_owned()) else { | ||
error!( | ||
"Invalid MAC address for device Symbol’s value as variable is void: {}: {:?}", | ||
device.0, address | ||
); | ||
return None; | ||
}; | ||
|
||
Some(address) | ||
} | ||
|
||
/// Test if a device is connected | ||
fn is_device_connected(device: &BluetoothDevice) -> bool { | ||
let Some(connected) = device.1.get("Connected") else { | ||
error!( | ||
"Connection property missing for device Symbol’s value as variable is void: {}", | ||
device.0 | ||
); | ||
return false; | ||
}; | ||
|
||
let Ok(connected) = <bool>::try_from(connected) else { | ||
error!( | ||
"Invalid connection property for device Symbol’s value as variable is void: {}: {:?}", | ||
device.0, connected | ||
); | ||
return false; | ||
}; | ||
|
||
connected | ||
} | ||
|
||
type Result<T> = std::result::Result<T, zbus::Error>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
pub mod bluetooth_monitor; | ||
|
||
use lazy_regex::regex_is_match; | ||
use std::fmt::Display; | ||
use thiserror::Error; | ||
|
||
#[derive(Debug, Error)] | ||
pub enum UtilError { | ||
#[error("Invalid MAC address: {0}")] | ||
InvalidMacAddress(String), | ||
} | ||
type Result<T> = std::result::Result<T, UtilError>; | ||
|
||
/// A MAC address | ||
pub struct MacAddress(String); | ||
|
||
impl Display for MacAddress { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
write!(f, "{}", self.0) | ||
} | ||
} | ||
|
||
impl std::fmt::Debug for MacAddress { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
Display::fmt(&self, f) | ||
} | ||
} | ||
|
||
impl TryFrom<String> for MacAddress { | ||
type Error = UtilError; | ||
|
||
fn try_from(value: String) -> Result<Self> { | ||
if regex_is_match!("([[:xdigit:]]{2}:){5}[[:xdigit:]]{2}", &value) { | ||
Ok(Self(value)) | ||
} else { | ||
Err(UtilError::InvalidMacAddress(value)) | ||
} | ||
} | ||
} |