use serde_derive::Deserialize;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use crossbeam_channel::Sender;
use dbus::{
arg::RefArg,
ffidisp::stdintf::org_freedesktop_dbus::{ObjectManager, Properties},
message::SignalArgs,
};
use crate::blocks::{Block, ConfigBlock, Update};
use crate::config::SharedConfig;
use crate::errors::*;
use crate::formatting::value::Value;
use crate::formatting::FormatTemplate;
use crate::input::{I3BarEvent, MouseButton};
use crate::scheduler::Task;
use crate::widgets::text::TextWidget;
use crate::widgets::{I3BarWidget, State};
pub struct BluetoothDevice {
pub path: String,
pub icon: Option<String>,
pub label: String,
con: dbus::ffidisp::Connection,
available: Arc<Mutex<bool>>,
}
impl BluetoothDevice {
pub fn new(mac: String, label: Option<String>) -> Result<Self> {
let con = dbus::ffidisp::Connection::get_private(dbus::ffidisp::BusType::System)
.block_error("bluetooth", "Failed to establish D-Bus connection.")?;
let objects = con
.with_path("org.bluez", "/", 1000)
.get_managed_objects()
.block_error("bluetooth", "Failed to get managed objects from org.bluez.")?;
let devices: Vec<(dbus::Path, String)> = objects
.into_iter()
.filter(|(_, interfaces)| interfaces.contains_key("org.bluez.Device1"))
.map(|(path, interfaces)| {
let props = interfaces.get("org.bluez.Device1").unwrap();
let address: String = props
.get("Address")
.unwrap()
.0
.as_str()
.unwrap()
.to_string();
(path, address)
})
.collect();
let mut initial_available = false;
let auto_path = devices
.into_iter()
.filter(|(_, address)| address == &mac)
.map(|(path, _)| path)
.next();
let path = if let Some(p) = auto_path {
initial_available = true;
p
} else {
dbus::strings::Path::new(format!("/org/bluez/hci0/dev_{}", mac.replace(":", "_")))
.unwrap()
}
.to_string();
let icon: Option<String> = con
.with_path("org.bluez", &path, 1000)
.get("org.bluez.Device1", "Icon")
.ok();
#[allow(clippy::mutex_atomic)]
let available = Arc::new(Mutex::new(initial_available));
Ok(BluetoothDevice {
path,
icon,
label: label.unwrap_or_else(|| "".to_string()),
con,
available,
})
}
pub fn battery(&self) -> Option<u8> {
self.con
.with_path("org.bluez", &self.path, 1000)
.get("org.bluez.Battery1", "Percentage")
.ok()
}
pub fn icon(&self) -> Option<String> {
self.con
.with_path("org.bluez", &self.path, 1000)
.get("org.bluez.Device1", "Icon")
.ok()
}
pub fn available(&self) -> Result<bool> {
Ok(*self
.available
.lock()
.block_error("bluetooth", "failed to acquire lock for `available`")?)
}
pub fn connected(&self) -> bool {
self.con
.with_path("org.bluez", &self.path, 1000)
.get("org.bluez.Device1", "Connected")
.unwrap_or(false)
}
pub fn toggle(&self) -> Result<()> {
let method = if self.connected() {
"Disconnect"
} else {
"Connect"
};
let msg =
dbus::Message::new_method_call("org.bluez", &self.path, "org.bluez.Device1", method)
.block_error("bluetooth", "Failed to build D-Bus method.")?;
let _ = self.con.send(msg);
Ok(())
}
pub fn monitor(&self, id: usize, update_request: Sender<Task>) {
let path_copy1 = self.path.clone();
let path_copy2 = self.path.clone();
let avail_copy1 = self.available.clone();
let avail_copy2 = self.available.clone();
let update_request_copy1 = update_request.clone();
let update_request_copy2 = update_request.clone();
let update_request_copy3 = update_request;
thread::Builder::new().name("bluetooth".into()).spawn(move || {
let c = dbus::blocking::Connection::new_system().unwrap();
use dbus::ffidisp::stdintf::org_freedesktop_dbus::ObjectManagerInterfacesAdded as IA;
let ma = IA::match_rule(Some(&"org.bluez".into()), None).static_clone();
c.add_match(ma, move |ia: IA, _, _| {
if ia.object == path_copy1.clone().into() {
let mut avail = avail_copy1.lock().unwrap();
*avail = true;
update_request_copy1
.send(Task {
id,
update_time: Instant::now(),
})
.unwrap();
}
true
})
.unwrap();
use dbus::ffidisp::stdintf::org_freedesktop_dbus::ObjectManagerInterfacesRemoved as IR;
let mr = IR::match_rule(Some(&"org.bluez".into()), None).static_clone();
c.add_match(mr, move |ir: IR, _, _| {
if ir.object == path_copy2.clone().into() {
let mut avail = avail_copy2.lock().unwrap();
*avail = false;
update_request_copy2
.send(Task {
id,
update_time: Instant::now(),
})
.unwrap();
}
true
})
.unwrap();
use dbus::ffidisp::stdintf::org_freedesktop_dbus::PropertiesPropertiesChanged as PPC;
let mr = PPC::match_rule(Some(&"org.bluez".into()), None).static_clone();
c.add_match(mr, move |_ppc: PPC, _, _| {
update_request_copy3
.send(Task {
id,
update_time: Instant::now(),
})
.unwrap();
true
})
.unwrap();
loop {
c.process(Duration::from_millis(1000)).unwrap();
}
}).unwrap();
}
}
pub struct Bluetooth {
id: usize,
output: TextWidget,
device: BluetoothDevice,
hide_disconnected: bool,
format: FormatTemplate,
format_unavailable: FormatTemplate,
}
#[derive(Deserialize, Debug, Default, Clone)]
#[serde(deny_unknown_fields)]
pub struct BluetoothConfig {
pub mac: String,
pub label: Option<String>,
#[serde(default = "BluetoothConfig::default_hide_disconnected")]
pub hide_disconnected: bool,
#[serde(default = "BluetoothConfig::default_format")]
pub format: String,
#[serde(default = "BluetoothConfig::default_format_unavailable")]
pub format_unavailable: String,
}
impl BluetoothConfig {
fn default_hide_disconnected() -> bool {
false
}
fn default_format() -> String {
"{label} {percentage}".into()
}
fn default_format_unavailable() -> String {
"{label} x".into()
}
}
impl ConfigBlock for Bluetooth {
type Config = BluetoothConfig;
fn new(
id: usize,
block_config: Self::Config,
shared_config: SharedConfig,
send: Sender<Task>,
) -> Result<Self> {
let device = BluetoothDevice::new(block_config.mac, block_config.label)?;
device.monitor(id, send);
Ok(Bluetooth {
id,
output: TextWidget::new(id, 0, shared_config).with_icon(match device.icon {
Some(ref icon) if icon == "audio-card" => "headphones",
Some(ref icon) if icon == "input-gaming" => "joystick",
Some(ref icon) if icon == "input-keyboard" => "keyboard",
Some(ref icon) if icon == "input-mouse" => "mouse",
_ => "bluetooth",
})?,
device,
hide_disconnected: block_config.hide_disconnected,
format: FormatTemplate::from_string(&block_config.format)?,
format_unavailable: FormatTemplate::from_string(&block_config.format_unavailable)?,
})
}
}
impl Block for Bluetooth {
fn id(&self) -> usize {
self.id
}
fn update(&mut self) -> Result<Option<Update>> {
if self.device.available()? {
let values = map!(
"{label}" => Value::from_string(self.device.label.clone()),
"{percentage}" => Value::from_integer(self.device.battery().unwrap_or(0) as i64).percents(),
);
let connected = self.device.connected();
self.output.set_text(self.device.label.to_string());
self.output
.set_state(if connected { State::Good } else { State::Idle });
self.output.set_icon(match self.device.icon() {
Some(ref icon) if icon == "audio-card" => "headphones",
Some(ref icon) if icon == "input-gaming" => "joystick",
Some(ref icon) if icon == "input-keyboard" => "keyboard",
Some(ref icon) if icon == "input-mouse" => "mouse",
_ => "bluetooth",
})?;
if let Some(value) = self.device.battery() {
self.output.set_state(match value {
0..=15 => State::Critical,
16..=30 => State::Warning,
31..=60 => State::Info,
61..=100 => State::Good,
_ => State::Warning,
});
}
self.output.set_text(self.format.render(&values)?);
} else {
let values = map!(
"{label}" => Value::from_string(self.device.label.clone()),
"{percentage}" => Value::from_string("".into()),
);
self.output.set_state(State::Idle);
self.output
.set_text(self.format_unavailable.render(&values)?);
}
Ok(None)
}
fn click(&mut self, event: &I3BarEvent) -> Result<()> {
if let MouseButton::Right = event.button {
self.device.toggle()?;
}
Ok(())
}
fn view(&self) -> Vec<&dyn I3BarWidget> {
if !self.device.connected() && self.hide_disconnected {
vec![]
} else {
vec![&self.output]
}
}
}