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
This commit is contained in:
Steven Ceuppens 2026-06-03 15:24:04 -06:00 committed by GitHub
parent de5e16d360
commit 989a56346a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 146 additions and 197 deletions

196
Cargo.lock generated
View file

@ -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"

View file

@ -17,23 +17,20 @@ use std::time::Duration;
use zbus::zvariant::OwnedObjectPath;
enum Dialog {
// RequestAuthorization {
// device: OwnedObjectPath,
// response: oneshot::Sender<bool>,
// },
RequestConfirmation {
device: String,
passkey: u32,
response: oneshot::Sender<bool>,
},
// RequestPasskey {
// device: OwnedObjectPath,
// response: oneshot::Sender<Option<u32>>,
// },
// RequestPinCode {
// device: OwnedObjectPath,
// response: oneshot::Sender<Option<String>>,
// },
DisplayPasskey {
device: String,
passkey: u32,
entered: u16,
},
DisplayPinCode {
device: String,
pincode: String,
},
}
#[derive(Default)]
@ -256,6 +253,85 @@ impl page::Page<crate::pages::Message> 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);
}

View file

@ -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

View file

@ -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?;