From 989a56346ab78faa74bed090b80ea8fc46b066aa Mon Sep 17 00:00:00 2001 From: Steven Ceuppens Date: Wed, 3 Jun 2026 15:24:04 -0600 Subject: [PATCH] fix(bluetooth): add passkey/PIN display dialogs for keyboard pairing Bluetooth keyboards require the host to display a passkey for the user to type. Previously this was not supported because the agent capability was set to DisplayYesNo and the DisplayPasskey/DisplayPinCode events were not handled. - Change agent capability to KeyboardDisplay - Add DisplayPasskey dialog with typed-key progress indicator - Add DisplayPinCode dialog for older keyboards - Auto-dismiss display dialogs when device reports Paired=true --- Cargo.lock | 196 ++------------------- cosmic-settings/src/pages/bluetooth/mod.rs | 139 +++++++++++++-- i18n/en/cosmic_settings.ftl | 6 + subscriptions/bluetooth/src/agent.rs | 2 +- 4 files changed, 146 insertions(+), 197 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 07cc34b..adddd09 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,7 +104,7 @@ dependencies = [ [[package]] name = "accounts-zbus" version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#507e342c21d3ce6ae41b1d4f3fa2f0ad5ee23e75" +source = "git+https://github.com/pop-os/dbus-settings-bindings#eed01dd3609e90e3c8cd043656734c500956c793" dependencies = [ "zbus", ] @@ -837,7 +837,7 @@ dependencies = [ [[package]] name = "bluez-zbus" version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#507e342c21d3ce6ae41b1d4f3fa2f0ad5ee23e75" +source = "git+https://github.com/pop-os/dbus-settings-bindings#eed01dd3609e90e3c8cd043656734c500956c793" dependencies = [ "futures-channel", "futures-util", @@ -1427,7 +1427,7 @@ dependencies = [ [[package]] name = "cosmic-dbus-a11y" version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#507e342c21d3ce6ae41b1d4f3fa2f0ad5ee23e75" +source = "git+https://github.com/pop-os/dbus-settings-bindings#eed01dd3609e90e3c8cd043656734c500956c793" dependencies = [ "zbus", ] @@ -1435,7 +1435,7 @@ dependencies = [ [[package]] name = "cosmic-dbus-networkmanager" version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#507e342c21d3ce6ae41b1d4f3fa2f0ad5ee23e75" +source = "git+https://github.com/pop-os/dbus-settings-bindings#eed01dd3609e90e3c8cd043656734c500956c793" dependencies = [ "bitflags 2.11.1", "derive_builder", @@ -1695,7 +1695,7 @@ dependencies = [ [[package]] name = "cosmic-settings-daemon" version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#507e342c21d3ce6ae41b1d4f3fa2f0ad5ee23e75" +source = "git+https://github.com/pop-os/dbus-settings-bindings#eed01dd3609e90e3c8cd043656734c500956c793" dependencies = [ "zbus", ] @@ -2858,23 +2858,10 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi 5.3.0", + "r-efi", "wasip2", ] -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", -] - [[package]] name = "gettext-rs" version = "0.7.7" @@ -3145,7 +3132,7 @@ checksum = "f558a64ac9af88b5ba400d99b579451af0d39c6d360980045b91aac966d705e2" [[package]] name = "hostname1-zbus" version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#507e342c21d3ce6ae41b1d4f3fa2f0ad5ee23e75" +source = "git+https://github.com/pop-os/dbus-settings-bindings#eed01dd3609e90e3c8cd043656734c500956c793" dependencies = [ "zbus", ] @@ -3878,12 +3865,6 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f2f8aeca682d874a5247084aa4fb7d1cef9ba45d889c21209a8818dcaaa0ec9" -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -4540,12 +4521,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "lebe" version = "0.5.3" @@ -4764,7 +4739,7 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "locale1" version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#507e342c21d3ce6ae41b1d4f3fa2f0ad5ee23e75" +source = "git+https://github.com/pop-os/dbus-settings-bindings#eed01dd3609e90e3c8cd043656734c500956c793" dependencies = [ "zbus", ] @@ -5107,7 +5082,7 @@ dependencies = [ [[package]] name = "nm-secret-agent-manager" version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#507e342c21d3ce6ae41b1d4f3fa2f0ad5ee23e75" +source = "git+https://github.com/pop-os/dbus-settings-bindings#eed01dd3609e90e3c8cd043656734c500956c793" dependencies = [ "zbus", ] @@ -5970,16 +5945,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -6149,12 +6114,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - [[package]] name = "rand" version = "0.8.5" @@ -7233,7 +7192,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.3.4", "once_cell", "rustix 1.1.4", "windows-sys 0.52.0", @@ -7345,7 +7304,7 @@ dependencies = [ [[package]] name = "timedate-zbus" version = "0.1.0" -source = "git+https://github.com/pop-os/dbus-settings-bindings#507e342c21d3ce6ae41b1d4f3fa2f0ad5ee23e75" +source = "git+https://github.com/pop-os/dbus-settings-bindings#eed01dd3609e90e3c8cd043656734c500956c793" dependencies = [ "zbus", ] @@ -7739,16 +7698,10 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "upower_dbus" version = "0.3.2" -source = "git+https://github.com/pop-os/dbus-settings-bindings#507e342c21d3ce6ae41b1d4f3fa2f0ad5ee23e75" +source = "git+https://github.com/pop-os/dbus-settings-bindings#eed01dd3609e90e3c8cd043656734c500956c793" dependencies = [ "serde", "serde_repr", @@ -7884,15 +7837,6 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] - [[package]] name = "wasm-bindgen" version = "0.2.118" @@ -7948,40 +7892,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap 2.14.0", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.15.5", - "indexmap 2.14.0", - "semver", -] - [[package]] name = "wasmtimer" version = "0.4.3" @@ -9045,88 +8955,6 @@ name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap 2.14.0", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.1", - "indexmap 2.14.0", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap 2.14.0", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] [[package]] name = "write16" diff --git a/cosmic-settings/src/pages/bluetooth/mod.rs b/cosmic-settings/src/pages/bluetooth/mod.rs index 7ffec0a..11a1712 100644 --- a/cosmic-settings/src/pages/bluetooth/mod.rs +++ b/cosmic-settings/src/pages/bluetooth/mod.rs @@ -17,23 +17,20 @@ use std::time::Duration; use zbus::zvariant::OwnedObjectPath; enum Dialog { - // RequestAuthorization { - // device: OwnedObjectPath, - // response: oneshot::Sender, - // }, RequestConfirmation { device: String, passkey: u32, response: oneshot::Sender, }, - // RequestPasskey { - // device: OwnedObjectPath, - // response: oneshot::Sender>, - // }, - // RequestPinCode { - // device: OwnedObjectPath, - // response: oneshot::Sender>, - // }, + DisplayPasskey { + device: String, + passkey: u32, + entered: u16, + }, + DisplayPinCode { + device: String, + pincode: String, + }, } #[derive(Default)] @@ -256,6 +253,85 @@ impl page::Page for Page { Some(dialog) } + + Dialog::DisplayPasskey { + device, + passkey, + entered, + } => { + let description = widget::text::body(fl!( + "bluetooth-display-passkey", + "description", + device = device + )) + .wrapping(Wrapping::Word); + + let passkey_str = format!("{passkey:06}"); + let entered = *entered as usize; + + let pin = widget::text::title1(passkey_str) + .width(Length::Fill) + .align_x(Alignment::Center) + .wrapping(Wrapping::None); + + let mut control = widget::column::with_capacity(3).push(description).push(pin); + + // Only show the key progress counter when the device actually + // reports keypress notifications. Most keyboards don't support + // this, so entered stays at 0 for the entire pairing process. + if entered > 0 { + let progress = widget::text::body(format!("{entered} / 6 keys entered")) + .width(Length::Fill) + .align_x(Alignment::Center) + .wrapping(Wrapping::None); + control = control.push(progress); + } + + let control = control.spacing(theme::spacing().space_xxs); + + let cancel_button = + widget::button::standard(fl!("cancel")).on_press(Message::PinCancel); + + let dialog = widget::dialog() + .title(fl!("bluetooth-display-passkey")) + .control(control) + .secondary_action(cancel_button) + .apply(Element::from) + .map(Into::into); + + Some(dialog) + } + + Dialog::DisplayPinCode { device, pincode } => { + let description = widget::text::body(fl!( + "bluetooth-display-pin", + "description", + device = device + )) + .wrapping(Wrapping::Word); + + let pin = widget::text::title1(pincode.clone()) + .width(Length::Fill) + .align_x(Alignment::Center) + .wrapping(Wrapping::None); + + let control = widget::column::with_capacity(2) + .push(description) + .push(pin) + .spacing(theme::spacing().space_xxs); + + let cancel_button = + widget::button::standard(fl!("cancel")).on_press(Message::PinCancel); + + let dialog = widget::dialog() + .title(fl!("bluetooth-display-pin")) + .control(control) + .secondary_action(cancel_button) + .apply(Element::from) + .map(Into::into); + + Some(dialog) + } } } } @@ -402,6 +478,19 @@ impl Page { } Event::UpdatedDevice(path, update) => { + // Dismiss passkey/pin display dialogs when pairing completes, + // since BlueZ may not always send Cancel after a successful pair. + if update + .iter() + .any(|u| matches!(u, DeviceUpdate::Paired(true))) + && matches!( + self.dialog, + Some(Dialog::DisplayPasskey { .. } | Dialog::DisplayPinCode { .. }) + ) + { + self.dialog = None; + } + if let Some(existing) = self.model.devices.get_mut(&path) { tracing::debug!("Device {} updated", existing.address); existing.update(update); @@ -465,6 +554,32 @@ impl Page { }); } + bluez_zbus::agent1::Message::DisplayPasskey { + device, + passkey, + entered, + } => { + let device = self.model.devices.get(&device).map_or_else( + || device.to_string(), + |device| device.alias_or_addr().to_owned(), + ); + + self.dialog = Some(Dialog::DisplayPasskey { + device, + passkey, + entered, + }); + } + + bluez_zbus::agent1::Message::DisplayPinCode { device, pincode } => { + let device = self.model.devices.get(&device).map_or_else( + || device.to_string(), + |device| device.alias_or_addr().to_owned(), + ); + + self.dialog = Some(Dialog::DisplayPinCode { device, pincode }); + } + bluez_zbus::agent1::Message::RequestPasskey { response, .. } => { _ = response.send(None); } diff --git a/i18n/en/cosmic_settings.ftl b/i18n/en/cosmic_settings.ftl index 535477b..ac509cd 100644 --- a/i18n/en/cosmic_settings.ftl +++ b/i18n/en/cosmic_settings.ftl @@ -252,6 +252,12 @@ bluetooth-paired = Previously connected devices bluetooth-confirm-pin = Confirm Bluetooth PIN .description = Please confirm that the following PIN matches the one displayed on { $device } +bluetooth-display-passkey = Bluetooth Pairing + .description = Please type the following passkey on { $device }, then press Enter + +bluetooth-display-pin = Bluetooth Pairing + .description = Please type the following PIN on { $device }, then press Enter + bluetooth-available = Nearby devices bluetooth-adapters = Bluetooth adapters diff --git a/subscriptions/bluetooth/src/agent.rs b/subscriptions/bluetooth/src/agent.rs index cb7e7db..99fcaae 100644 --- a/subscriptions/bluetooth/src/agent.rs +++ b/subscriptions/bluetooth/src/agent.rs @@ -40,7 +40,7 @@ pub async fn watch( bluez .register_agent( &agent_path, - <&'static str>::from(bluez_zbus::agent1::Capability::DisplayYesNo), + <&'static str>::from(bluez_zbus::agent1::Capability::KeyboardDisplay), ) .await?;