From 9c1a7fbc82143552367e178f3949f481c615ef61 Mon Sep 17 00:00:00 2001 From: Kheven Colino <90728780+khevencolino@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:06:07 -0300 Subject: [PATCH] feat(bluetooth): add ability to rename Bluetooth devices (#2160) --- cosmic-settings/src/pages/bluetooth/mod.rs | 79 +++++++++++++++++++++- i18n/en/cosmic_settings.ftl | 3 + subscriptions/bluetooth/src/device.rs | 36 +++++++++- subscriptions/bluetooth/src/lib.rs | 1 + 4 files changed, 115 insertions(+), 4 deletions(-) diff --git a/cosmic-settings/src/pages/bluetooth/mod.rs b/cosmic-settings/src/pages/bluetooth/mod.rs index 66319e4..ca5dc6e 100644 --- a/cosmic-settings/src/pages/bluetooth/mod.rs +++ b/cosmic-settings/src/pages/bluetooth/mod.rs @@ -37,6 +37,10 @@ enum Dialog { device: String, pincode: String, }, + RenameDevice { + path: OwnedObjectPath, + name: String, + }, } #[derive(Default)] @@ -353,6 +357,29 @@ impl page::Page for Page { Some(dialog) } + + Dialog::RenameDevice { name, .. } => { + let is_valid = is_valid_bluetooth_alias(name); + let input = widget::text_input("", name) + .on_input(|value| Message::RenameDeviceInput(value)) + .on_submit(|_| Message::RenameDeviceConfirm); + + let rename_button = + widget::button::suggested(fl!("rename")).on_press_maybe(is_valid.then_some(Message::RenameDeviceConfirm)); + + let cancel_button = + widget::button::standard(fl!("cancel")).on_press(Message::RenameDeviceCancel); + + let dialog = widget::dialog() + .title(fl!("bluetooth-rename-device")) + .control(input) + .primary_action(rename_button) + .secondary_action(cancel_button) + .apply(Element::from) + .map(Into::into); + + Some(dialog) + } } } } @@ -369,6 +396,10 @@ pub enum Message { PinConfirm, PopupDevice(Option), PopupSetting(bool), + RenameDevice(OwnedObjectPath), + RenameDeviceInput(String), + RenameDeviceConfirm, + RenameDeviceCancel, SelectAdapter(Option), ServiceActivate, ServiceEnable, @@ -629,6 +660,10 @@ impl Page { _ => (), } + }, + + Event::DeviceRenameFailed(path) => { + tracing::warn!("Failed to rename device {path}"); } }, @@ -836,6 +871,38 @@ impl Page { } } + Message::RenameDevice(path) => { + self.model.popup_device = None; + let name = self + .model + .devices + .get(&path) + .map_or_else(String::new, |d| d.alias_or_addr().to_owned()); + self.dialog = Some(Dialog::RenameDevice { path, name }); + } + + Message::RenameDeviceInput(new_name) => { + if let Some(Dialog::RenameDevice { name, .. }) = &mut self.dialog { + *name = new_name; + } + } + + Message::RenameDeviceCancel => { + if matches!(self.dialog, Some(Dialog::RenameDevice { .. })) { + self.dialog = None; + } + } + + Message::RenameDeviceConfirm => { + if let Some(Dialog::RenameDevice { path, name }) = self.dialog.take() { + if let Some(connection) = self.connection.clone() { + return cosmic::task::future(rename_device(connection, path, name.trim().into())); + } else { + tracing::warn!("No DBus connection ready"); + } + } + } + Message::ServiceActivate => { let activate_future = self.service_manager.activate(); return cosmic::task::future(async move { @@ -934,6 +1001,11 @@ fn status() -> Section { }) } +fn is_valid_bluetooth_alias(name: &str) -> bool { + let trimmed = name.trim(); + !trimmed.is_empty() && trimmed.len() <= 248 +} + fn popup_button(message: Option, text: &str) -> Element<'_, Message> { let spacing = theme::spacing(); widget::text::body(text) @@ -954,6 +1026,7 @@ fn connected_devices() -> Section { device_connect = fl!("bluetooth", "connect"); device_disconnect = fl!("bluetooth", "disconnect"); device_forget = fl!("bluetooth", "forget"); + device_rename = fl!("rename"); }); Section::default() @@ -991,13 +1064,17 @@ fn connected_devices() -> Section { .position(widget::popover::Position::Bottom) .on_close(Message::PopupDevice(None)) .popup( - widget::column::with_capacity(2) + widget::column::with_capacity(3) .push_maybe(device.is_connected().then(|| { popup_button( Some(Message::DisconnectDevice(path.clone())), &descriptions[device_disconnect], ) })) + .push(popup_button( + Some(Message::RenameDevice(path.clone())), + &descriptions[device_rename], + )) .push_maybe(device.paired.then(|| { popup_button( Some(Message::ForgetDevice(path.clone())), diff --git a/i18n/en/cosmic_settings.ftl b/i18n/en/cosmic_settings.ftl index 860d19d..44e24bd 100644 --- a/i18n/en/cosmic_settings.ftl +++ b/i18n/en/cosmic_settings.ftl @@ -233,6 +233,7 @@ online-accounts = Online accounts activate = Activate confirm = Confirm enable = Enable +rename = Rename bluetooth = Bluetooth .status = This system is visible as { $aliases } while Bluetooth settings are open. @@ -260,6 +261,8 @@ bluetooth-display-passkey = Bluetooth Pairing bluetooth-display-pin = Bluetooth Pairing .description = Please type the following PIN on { $device }, then press Enter +bluetooth-rename-device = Rename Device + bluetooth-available = Nearby devices bluetooth-adapters = Bluetooth adapters diff --git a/subscriptions/bluetooth/src/device.rs b/subscriptions/bluetooth/src/device.rs index 2ba1854..6e4b21b 100644 --- a/subscriptions/bluetooth/src/device.rs +++ b/subscriptions/bluetooth/src/device.rs @@ -32,7 +32,7 @@ fn device_type_to_icon(device_type: &str) -> &'static str { #[derive(Default, Debug, Clone)] pub struct Device { - alias: Option, + pub alias: Option, pub address: String, pub adapter: OwnedObjectPath, pub enabled: Active, @@ -46,7 +46,7 @@ impl Device { let (address, adapter, alias) = join!( proxy.device.address(), proxy.device.adapter(), - proxy.device.name() + proxy.device.alias() ); let address = address?; if address.is_empty() { @@ -56,7 +56,10 @@ impl Device { if adapter.is_empty() { return Err(zbus::Error::Failure("Device has no adapter".to_owned())); } - let alias = alias.ok(); + let alias = match alias { + Ok(alias) if !alias.is_empty() && alias != address.replace(':', "-") => Some(alias), + _ => proxy.device.name().await.ok(), + }; let device_type: String = proxy.icon().await; let paired = proxy.device.paired().await.unwrap_or(false); let enabled = if proxy.device.connected().await.unwrap_or(false) { @@ -309,6 +312,33 @@ pub async fn forget_device(connection: zbus::Connection, device_path: OwnedObjec } } +pub async fn rename_device( + connection: zbus::Connection, + device_path: OwnedObjectPath, + name: String, +) -> Event { + let proxy = match bluez_zbus::get_device(&connection, device_path.clone()).await { + Err(why) => { + tracing::error!("Unable to get the device: {why}"); + return Event::DeviceFailed(device_path); + } + Ok(proxy) => proxy, + }; + + for attempt in 1..5 { + let result = proxy.device.set_alias(&name).await; + + if let Err(why) = result { + tracing::warn!("Unable to rename device: {why}"); + tokio::time::sleep(Duration::from_millis(1000 * attempt)).await; + } else { + return Event::Ok; + } + } + + Event::DeviceRenameFailed(device_path) +} + pub async fn get_devices(connection: zbus::Connection, adapter_path: OwnedObjectPath) -> Event { // TODO error handling let result: zbus::Result> = async { diff --git a/subscriptions/bluetooth/src/lib.rs b/subscriptions/bluetooth/src/lib.rs index f4263ac..e6f0636 100644 --- a/subscriptions/bluetooth/src/lib.rs +++ b/subscriptions/bluetooth/src/lib.rs @@ -21,6 +21,7 @@ pub enum Event { DBusError(zbus::Error), DBusServiceUnknown, DeviceFailed(OwnedObjectPath), + DeviceRenameFailed(OwnedObjectPath), Ok, NameHasNoOwner, RemovedAdapter(OwnedObjectPath),