Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
WIP: feat(eww): Add bluetooth indicator
Browse files Browse the repository at this point in the history
TLATER committed Jan 8, 2025
1 parent 2b5880b commit bf6c15a
Showing 8 changed files with 1,627 additions and 3 deletions.
9 changes: 7 additions & 2 deletions .dir-locals.el
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"]))))))
20 changes: 19 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
@@ -132,7 +132,17 @@
let
inherit (sops-nix.packages.x86_64-linux) sops-init-gpg-key sops-import-keys-hook;
inherit (self.packages.x86_64-linux) commit-nvfetcher;
inherit (nixpkgs.legacyPackages.x86_64-linux) nvchecker nvfetcher;
inherit (nixpkgs.legacyPackages.x86_64-linux)
cargo
clippy
dbus
nvchecker
nvfetcher
pkg-config
rust-analyzer
rustc
rustfmt
;
home-manager-bin = home-manager.packages.x86_64-linux.default;
in
nixpkgs.legacyPackages.x86_64-linux.mkShell {
@@ -142,6 +152,14 @@
commit-nvfetcher
home-manager-bin
sops-init-gpg-key

cargo
clippy
dbus
rust-analyzer
rustc
rustfmt
pkg-config
];

sopsPGPKeyDirs = [
1 change: 1 addition & 0 deletions home-config/dotfiles/eww/utils/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
target/
1,377 changes: 1,377 additions & 0 deletions home-config/dotfiles/eww/utils/Cargo.lock

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions home-config/dotfiles/eww/utils/Cargo.toml
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 home-config/dotfiles/eww/utils/src/bin/bluetooth-monitor.rs
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 home-config/dotfiles/eww/utils/src/bluetooth_monitor.rs
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>;
39 changes: 39 additions & 0 deletions home-config/dotfiles/eww/utils/src/lib.rs
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))
}
}
}

0 comments on commit bf6c15a

Please sign in to comment.