diff --git a/cosmic-applet-bluetooth/src/app.rs b/cosmic-applet-bluetooth/src/app.rs index 627f3809..eb03ddb7 100644 --- a/cosmic-applet-bluetooth/src/app.rs +++ b/cosmic-applet-bluetooth/src/app.rs @@ -34,6 +34,7 @@ use crate::{ static BLUETOOTH_ENABLED: Lazy = Lazy::new(id::Toggler::unique); +#[inline] pub fn run() -> cosmic::iced::Result { cosmic::applet::run::(()) } @@ -53,6 +54,7 @@ struct CosmicBluetoothApplet { } impl CosmicBluetoothApplet { + #[inline] fn update_icon(&mut self) { self.icon_name = if self.bluer_state.bluetooth_enabled { "cosmic-applet-bluetooth-active-symbolic" @@ -117,7 +119,7 @@ impl cosmic::Application for CosmicBluetoothApplet { self.popup.replace(new_id); self.timeline = Timeline::new(); - let mut popup_settings = self.core.applet.get_popup_settings( + let popup_settings = self.core.applet.get_popup_settings( self.core.main_window_id().unwrap(), new_id, None, @@ -348,6 +350,7 @@ impl cosmic::Application for CosmicBluetoothApplet { } = theme::active().cosmic().spacing; let mut known_bluetooth = vec![]; + // PERF: This should be pre-filtered in an update. for dev in self.bluer_state.devices.iter().filter(|d| { !self .request_confirmation @@ -355,7 +358,7 @@ impl cosmic::Application for CosmicBluetoothApplet { .map_or(false, |(dev, _, _)| d.address == dev.address) }) { let mut row = row![ - icon::from_name(dev.icon.as_str()).size(16).symbolic(true), + icon::from_name(dev.icon).size(16).symbolic(true), text::body(dev.name.clone()) .align_x(Alignment::Start) .align_y(Alignment::Center) @@ -364,12 +367,8 @@ impl cosmic::Application for CosmicBluetoothApplet { .align_y(Alignment::Center) .spacing(12); - if let Some(DeviceProperty::BatteryPercentage(battery)) = dev - .properties - .iter() - .find(|p| matches!(p, DeviceProperty::BatteryPercentage(_))) - { - let icon = match *battery { + if let Some(battery) = dev.battery_percent { + let icon = match battery { b if b >= 20 && b < 40 => "battery-low", b if b < 20 => "battery-caution", _ => "battery", @@ -474,9 +473,7 @@ impl cosmic::Application for CosmicBluetoothApplet { if let Some((device, pin, _)) = self.request_confirmation.as_ref() { let row = column![ padded_control(row![ - icon::from_name(device.icon.as_str()) - .size(16) - .symbolic(true), + icon::from_name(device.icon).size(16).symbolic(true), text::body(&device.name) .align_x(Alignment::Start) .align_y(Alignment::Center) @@ -528,7 +525,7 @@ impl cosmic::Application for CosmicBluetoothApplet { && (d.has_name() || d.is_known_device_type()) }) { let row = row![ - icon::from_name(dev.icon.as_str()).size(16).symbolic(true), + icon::from_name(dev.icon).size(16).symbolic(true), text::body(dev.name.clone()).align_x(Alignment::Start), ] .align_y(Alignment::Center) diff --git a/cosmic-applet-bluetooth/src/bluetooth.rs b/cosmic-applet-bluetooth/src/bluetooth.rs index bb28b9a7..f4a5bb29 100644 --- a/cosmic-applet-bluetooth/src/bluetooth.rs +++ b/cosmic-applet-bluetooth/src/bluetooth.rs @@ -18,6 +18,7 @@ use cosmic::{ iced_futures::stream, }; +use futures::{stream::FuturesUnordered, FutureExt}; use rand::Rng; use tokio::{ spawn, @@ -28,8 +29,8 @@ use tokio::{ task::JoinHandle, time::timeout, }; - // Copied from https://github.com/bluez/bluez/blob/39467578207889fd015775cbe81a3db9dd26abea/src/dbus-common.c#L53 +#[inline] fn device_type_to_icon(device_type: &str) -> &'static str { match device_type { "computer" => "laptop-symbolic", @@ -49,6 +50,7 @@ fn device_type_to_icon(device_type: &str) -> &'static str { } } +#[inline] pub fn bluetooth_subscription( id: I, ) -> iced::Subscription { @@ -70,7 +72,7 @@ pub fn bluetooth_subscription( .await; }; - let state = session_state.bluer_state().await; + let state = bluer_state(&session_state.adapter).await; // reconnect to paired and trusted devices if state.bluetooth_enabled { @@ -90,29 +92,28 @@ pub fn bluetooth_subscription( }) .await; - let mut event_handler = async |event| match event { - BluerSessionEvent::ChangesProcessed(state) => { - _ = output.send(BluerEvent::DevicesChanged { state }).await; - } + let mut event_handler = async |event| { + let message = match event { + BluerSessionEvent::ChangesProcessed(state) => { + BluerEvent::DevicesChanged { state } + } - BluerSessionEvent::RequestResponse { - req, - state, - err_msg, - } => { - _ = output - .send(BluerEvent::RequestResponse { - req, - state, - err_msg, - }) - .await; - } - BluerSessionEvent::AgentEvent(e) => { - _ = output.send(BluerEvent::AgentEvent(e)).await; - } + BluerSessionEvent::RequestResponse { + req, + state, + err_msg, + } => BluerEvent::RequestResponse { + req, + state, + err_msg, + }, - _ => {} + BluerSessionEvent::AgentEvent(e) => BluerEvent::AgentEvent(e), + + _ => return, + }; + + _ = output.send(message).await; }; let mut interval = tokio::time::interval(Duration::from_secs(1)); @@ -124,6 +125,7 @@ pub fn bluetooth_subscription( if let Some(event) = session_rx.recv().await { event_handler(event).await; + // Consume any additional available events. while let Some(event) = session_rx.try_recv().ok() { event_handler(event).await; } @@ -190,15 +192,18 @@ pub enum BluerDeviceStatus { #[derive(Debug, Clone)] pub struct BluerDevice { pub name: String, + pub icon: &'static str, pub address: Address, pub status: BluerDeviceStatus, - pub properties: Vec, - pub icon: String, + pub battery_percent: Option, + pub is_paired: bool, + pub is_trusted: bool, } impl Eq for BluerDevice {} impl Ord for BluerDevice { + #[inline] fn cmp(&self, other: &Self) -> std::cmp::Ordering { match self.status.cmp(&other.status) { std::cmp::Ordering::Equal => self.name.to_lowercase().cmp(&other.name.to_lowercase()), @@ -208,6 +213,7 @@ impl Ord for BluerDevice { } impl PartialOrd for BluerDevice { + #[inline] fn partial_cmp(&self, other: &Self) -> Option { match self.status.cmp(&other.status) { std::cmp::Ordering::Equal => { @@ -219,6 +225,7 @@ impl PartialOrd for BluerDevice { } impl PartialEq for BluerDevice { + #[inline] fn eq(&self, other: &Self) -> bool { self.name == other.name && self.address == other.address } @@ -227,18 +234,26 @@ impl PartialEq for BluerDevice { const DEFAULT_DEVICE_ICON: &str = "bluetooth-symbolic"; impl BluerDevice { + #[inline(never)] pub async fn from_device(device: &bluer::Device) -> Self { - let mut name = device - .name() - .await - .unwrap_or_default() - .unwrap_or_else(|| device.address().to_string()); + let (mut name, is_paired, is_trusted, is_connected, battery_percent, icon) = futures::join!( + device.name().map(|res| res + .ok() + .flatten() + .unwrap_or_else(|| device.address().to_string())), + device.is_paired().map(Result::unwrap_or_default), + device.is_trusted().map(Result::unwrap_or_default), + device.is_connected().map(Result::unwrap_or_default), + device.battery_percentage().map(|res| res.ok().flatten()), + device + .icon() + .map(|res| device_type_to_icon(&res.ok().flatten().unwrap_or_default())) + ); + if name.is_empty() { name = device.address().to_string(); }; - let is_paired = device.is_paired().await.unwrap_or_default(); - let is_connected = device.is_connected().await.unwrap_or_default(); - let properties = device.all_properties().await.unwrap_or_default(); + let status = if is_connected { BluerDeviceStatus::Connected } else if is_paired { @@ -246,44 +261,30 @@ impl BluerDevice { } else { BluerDeviceStatus::Disconnected }; - let icon = properties - .iter() - .find_map(|p| { - if let DeviceProperty::Icon(icon) = p { - Some(device_type_to_icon(icon.clone().as_str()).to_string()) - } else { - None - } - }) - .unwrap_or_else(|| device_type_to_icon(DEFAULT_DEVICE_ICON).to_string()); Self { name, + icon, address: device.address(), status, - properties, - icon, + battery_percent, + is_paired, + is_trusted, } } + #[inline] fn paired_and_trusted(&self) -> bool { - self.properties - .iter() - .filter(|p| { - matches!( - p, - DeviceProperty::Trusted(true) | DeviceProperty::Paired(true) - ) - }) - .count() - == 2 + self.is_paired && self.is_trusted } + #[inline] #[must_use] pub fn is_known_device_type(&self) -> bool { self.icon != DEFAULT_DEVICE_ICON } + #[inline] #[must_use] pub fn has_name(&self) -> bool { self.name != self.address.to_string() @@ -326,9 +327,8 @@ pub struct BluerSessionState { } impl BluerSessionState { - pub(crate) async fn new(session: Session) -> anyhow::Result { + async fn new(session: Session) -> anyhow::Result { let adapter = session.default_adapter().await?; - let devices = build_device_list(&adapter).await; let (tx, rx) = tokio::sync::mpsc::channel(100); let (req_tx, req_rx) = channel(100); let tx_clone_1 = tx.clone(); @@ -524,6 +524,7 @@ impl BluerSessionState { Ok(self_) } + #[inline] fn listen_bluetooth_power_changes(&self) { let tx = self.tx.clone(); let req_tx = self.req_tx.clone(); @@ -532,13 +533,15 @@ impl BluerSessionState { let _handle: JoinHandle> = spawn(async move { let mut status = adapter_clone.is_powered().await.unwrap_or_default(); let mut interval = tokio::time::interval(Duration::from_secs(3)); + let mut devices = Vec::new(); loop { interval.tick().await; let new_status = adapter_clone.is_powered().await.unwrap_or_default(); + devices = build_device_list(devices, &adapter_clone).await; if new_status != status { status = new_status; let state = BluerState { - devices: build_device_list(&adapter_clone).await, + devices: devices.clone(), bluetooth_enabled: status, }; if state.bluetooth_enabled { @@ -555,7 +558,8 @@ impl BluerSessionState { }); } - pub(crate) fn process_changes(&mut self) { + #[inline] + fn process_changes(&mut self) { let tx = self.tx.clone(); let req_tx = self.req_tx.clone(); let Some(mut wake_up) = self.wake_up_discover_rx.take() else { @@ -565,8 +569,8 @@ impl BluerSessionState { let adapter_clone = self.adapter.clone(); let _monitor_devices: tokio::task::JoinHandle> = spawn( async move { - let mut milli_timeout = 10; let mut change_stream = { + let mut milli_timeout = 10; let mut res = adapter_clone.discover_devices_with_changes().await; while res.is_err() { _ = tokio::time::timeout( @@ -577,14 +581,15 @@ impl BluerSessionState { res = adapter_clone.discover_devices_with_changes().await; milli_timeout = milli_timeout.saturating_mul(5); } - milli_timeout = 10; res.unwrap() }; let mut interval = tokio::time::interval(Duration::from_secs(1)); loop { + let mut milli_timeout = 10; let mut devices: Vec = Vec::new(); + let mut new_devices = Vec::new(); 'outer: loop { tokio::select! { change = timeout(Duration::from_millis(milli_timeout), change_stream.next()) => { @@ -601,35 +606,34 @@ impl BluerSessionState { } }; - let mut new_devices = build_device_list(&adapter_clone).await; + new_devices = build_device_list(new_devices, &adapter_clone).await; for d in new_devices .iter() .filter(|d| !devices.contains(d) && d.paired_and_trusted()) { _ = req_tx.send(BluerRequest::ConnectDevice(d.address)).await; } - devices = mem::take(&mut new_devices); + mem::swap(&mut new_devices, &mut devices); let _ = tx .send(BluerSessionEvent::ChangesProcessed(BluerState { - devices: build_device_list(&adapter_clone).await, + devices: devices.clone(), bluetooth_enabled: adapter_clone .is_powered() .await .unwrap_or_default(), })) .await; - // reset timeout - milli_timeout = 10; + interval.tick().await; } let _ = tx.send(BluerSessionEvent::ChangeStreamEnded).await; - interval.tick().await; } }, ); } - pub(crate) fn process_requests(&self, request_rx: Receiver) { + #[inline] + fn process_requests(&self, request_rx: Receiver) { let active_requests = self.active_requests.clone(); let adapter = self.adapter.clone(); let tx = self.tx.clone(); @@ -740,15 +744,10 @@ impl BluerSessionState { BluerRequest::StateUpdate => {} }; - let state = BluerState { - devices: build_device_list(&adapter_clone).await, - bluetooth_enabled: adapter_clone.is_powered().await.unwrap_or_default(), - }; - let _ = tx_clone .send(BluerSessionEvent::RequestResponse { req: req_clone, - state, + state: bluer_state(&adapter_clone).await, err_msg, }) .await; @@ -763,28 +762,42 @@ impl BluerSessionState { Ok(()) }); } +} - pub(crate) async fn bluer_state(&self) -> BluerState { - BluerState { - devices: build_device_list(&self.adapter).await, - // TODO is this a proper way of checking if bluetooth is enabled? - bluetooth_enabled: self.adapter.is_powered().await.unwrap_or_default(), - } +#[inline] +async fn bluer_state(adapter: &Adapter) -> BluerState { + let (devices, bluetooth_enabled) = futures::join!( + build_device_list(Vec::new(), adapter), + // TODO is this a proper way of checking if bluetooth is enabled? + adapter.is_powered().map(Result::unwrap_or_default), + ); + + BluerState { + devices, + bluetooth_enabled, } } -async fn build_device_list(adapter: &Adapter) -> Vec { +#[inline(never)] +async fn build_device_list(mut devices: Vec, adapter: &Adapter) -> Vec { let addrs = adapter.device_addresses().await.unwrap_or_default(); - let mut devices = Vec::with_capacity(addrs.len()); - for address in addrs { - let device = match adapter.device(address) { - Ok(device) => device, - Err(_) => continue, - }; - - devices.push(BluerDevice::from_device(&device).await); + devices.clear(); + if addrs.len() > devices.capacity() { + devices.reserve(addrs.len() - devices.capacity()); } + + // Concurrently collect bluer devices from each address. + let mut device_stream = addrs + .into_iter() + .filter_map(|address| adapter.device(address).ok()) + .map(async move |device| BluerDevice::from_device(&device).await) + .collect::>(); + + while let Some(device) = device_stream.next().await { + devices.push(device) + } + devices.sort(); devices } diff --git a/cosmic-applet-bluetooth/src/lib.rs b/cosmic-applet-bluetooth/src/lib.rs index 6a614230..116a967a 100644 --- a/cosmic-applet-bluetooth/src/lib.rs +++ b/cosmic-applet-bluetooth/src/lib.rs @@ -8,6 +8,7 @@ mod localize; use crate::localize::localize; +#[inline] pub fn run() -> cosmic::iced::Result { localize(); app::run() diff --git a/cosmic-applet-bluetooth/src/localize.rs b/cosmic-applet-bluetooth/src/localize.rs index b69ee397..95cd5aac 100644 --- a/cosmic-applet-bluetooth/src/localize.rs +++ b/cosmic-applet-bluetooth/src/localize.rs @@ -34,6 +34,7 @@ macro_rules! fl { } // Get the `Localizer` to be used for localizing this library. +#[inline] pub fn localizer() -> Box { Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations)) }