What changed, and why it matters
This commit is a routine user-interface feature update for the Trezor hardware wallet. It changes the device menu so that paired Bluetooth-style devices can display both a host name and an application name, and it adds a scrolling (marquee) text effect for long names. There is no security-relevant change, no vulnerability fix, and no indication of a security issue.
No security action required. Treat as normal feature/UI code review.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch refactors the show_device_menu API across multiple UI layouts. The paired_devices parameter changes from Iterable[tuple[str, str | None]] to Iterable[tuple[str, tuple[str, str] | None]], carrying both a host name and an app name. The Eckhart layout’s button and device-menu screen are updated to render the app name as primary text and the host name as a marquee subtext, with fallback labels for unknown/disconnected states. Bounds handling for marquee placement is also adjusted. No cryptographic, authentication, or trust-boundary logic is modified.
Changed components
core/embed/rust/src/ui/api/firmware_micropython.rscore/embed/rust/src/ui/layout_bolt/ui_firmware.rscore/embed/rust/src/ui/layout_caesar/ui_firmware.rscore/embed/rust/src/ui/layout_delizia/ui_firmware.rscore/embed/rust/src/ui/layout_eckhart/component/button.rscore/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rscore/embed/rust/src/ui/layout_eckhart/ui_firmware.rscore/embed/rust/src/ui/ui_firmware.rscore/mocks/generated/trezorui_api.pyiInspect captured patch +129 / −67
diff --git a/core/embed/rust/src/ui/api/firmware_micropython.rs b/core/embed/rust/src/ui/api/firmware_micropython.rs
index 3c76e174..661f43b0 100644
--- a/core/embed/rust/src/ui/api/firmware_micropython.rs
+++ b/core/embed/rust/src/ui/api/firmware_micropython.rs
@@ -1,4 +1,5 @@
use crate::{
+ error::Error,
io::BinaryData,
micropython::{
buffer::StrBuffer,
@@ -945,14 +946,20 @@ extern "C" fn new_show_device_menu(n_args: usize, args: *const Obj, kwargs: *mut
let backup_needed: bool = kwargs.get(Qstr::MP_QSTR_backup_needed)?.try_into()?;
let paired_obj: Obj = kwargs.get(Qstr::MP_QSTR_paired_devices)?;
let mut paired_devices: heapless::Vec<
- (TString<'static>, Option<TString<'static>>),
+ (TString<'static>, Option<[TString; 2]>),
MAX_PAIRED_DEVICES,
> = heapless::Vec::new();
for device in IterBuf::new().try_iterate(paired_obj)? {
- let [mac, name]: [Obj; 2] = util::iter_into_array(device)?;
+ let [mac, host_info]: [Obj; 2] = util::iter_into_array(device)?;
let mac: TString<'static> = mac.try_into()?;
- let name: Option<TString<'static>> = name.try_into_option()?;
- unwrap!(paired_devices.push((mac, name)));
+ let host_info: Option<[TString<'static>; 2]> = host_info
+ .try_into_option()?
+ .map(util::iter_into_array)
+ .transpose()?;
+
+ if paired_devices.push((mac, host_info)).is_err() {
+ return Err(Error::OutOfRange);
+ }
}
let connected_idx: Option<u8> =
kwargs.get(Qstr::MP_QSTR_connected_idx)?.try_into_option()?;
@@ -1936,7 +1943,7 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// init_submenu_idx: int | None,
/// backup_failed: bool,
/// backup_needed: bool,
- /// paired_devices: Iterable[tuple[str, str | None]],
+ /// paired_devices: Iterable[tuple[str, tuple[str, str] | None]],
/// connected_idx: int | None,
/// pin_enabled: bool | None,
/// auto_lock: tuple[str, str] | None,
diff --git a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
index c5285cba..4a4e4c65 100644
--- a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
@@ -939,7 +939,7 @@ impl FirmwareUI for UIBolt {
_backup_failed: bool,
_backup_needed: bool,
_paired_devices: heapless::Vec<
- (TString<'static>, Option<TString<'static>>),
+ (TString<'static>, Option<[TString<'static>; 2]>),
MAX_PAIRED_DEVICES,
>,
_connected_idx: Option<u8>,
diff --git a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
index 0c8329a1..a13bcc89 100644
--- a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
@@ -1136,7 +1136,7 @@ impl FirmwareUI for UICaesar {
_backup_failed: bool,
_backup_needed: bool,
_paired_devices: heapless::Vec<
- (TString<'static>, Option<TString<'static>>),
+ (TString<'static>, Option<[TString<'static>; 2]>),
MAX_PAIRED_DEVICES,
>,
_connected_idx: Option<u8>,
diff --git a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
index 0bb902da..679c4734 100644
--- a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
@@ -1021,7 +1021,7 @@ impl FirmwareUI for UIDelizia {
_backup_failed: bool,
_backup_needed: bool,
_paired_devices: heapless::Vec<
- (TString<'static>, Option<TString<'static>>),
+ (TString<'static>, Option<[TString<'static>; 2]>),
MAX_PAIRED_DEVICES,
>,
_connected_idx: Option<u8>,
diff --git a/core/embed/rust/src/ui/layout_eckhart/component/button.rs b/core/embed/rust/src/ui/layout_eckhart/component/button.rs
index a67c762d..e9fc82da 100644
--- a/core/embed/rust/src/ui/layout_eckhart/component/button.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/component/button.rs
@@ -147,30 +147,35 @@ impl Button {
subtext: Option<TString<'static>>,
connected: bool,
) -> Self {
- let (icon, subtext_style) = if connected {
+ let icon_color = if connected {
+ theme::GREEN_LIGHT
+ } else {
+ theme::GREY_DARK
+ };
+ let (subtext, subtext_style) = if let Some(subtext) = subtext {
+ (subtext, &theme::TEXT_MENU_ITEM_SUBTITLE)
+ } else if connected {
(
- (theme::ICON_SQUARE, theme::GREEN_LIGHT),
+ TR::words__connected.into(),
&theme::TEXT_MENU_ITEM_SUBTITLE_GREEN,
)
} else {
(
- (theme::ICON_SQUARE, theme::GREY_DARK),
+ TR::words__disconnected.into(),
&theme::TEXT_MENU_ITEM_SUBTITLE,
)
};
- let subtext = subtext.unwrap_or_else(|| {
- if connected {
- TR::words__connected.into()
- } else {
- TR::words__disconnected.into()
- }
- });
- Self::with_text_and_subtext(text, subtext, subtext_style, Some(icon))
- .with_text_align(Self::MENU_ITEM_ALIGNMENT)
- .with_content_offset(Self::MENU_ITEM_CONTENT_OFFSET)
- .styled(stylesheet)
- .with_radius(Self::MENU_ITEM_RADIUS)
+ Self::with_text_and_subtext_marquee(
+ text,
+ subtext,
+ subtext_style,
+ Some((theme::ICON_SQUARE, icon_color)),
+ )
+ .with_text_align(Self::MENU_ITEM_ALIGNMENT)
+ .with_content_offset(Self::MENU_ITEM_CONTENT_OFFSET)
+ .styled(stylesheet)
+ .with_radius(Self::MENU_ITEM_RADIUS)
}
pub const fn with_single_line_text(text: TString<'static>) -> Self {
@@ -209,6 +214,20 @@ impl Button {
))
}
+ pub fn with_text_and_subtext_marquee(
+ text: TString<'static>,
+ subtext: TString<'static>,
+ subtext_style: &'static TextStyle,
+ icon: Option<(Icon, Color)>,
+ ) -> Self {
+ Self::new(ButtonContent::text_and_marquee_subtext(
+ text,
+ subtext,
+ subtext_style,
+ icon,
+ ))
+ }
+
pub const fn with_icon(icon: Icon) -> Self {
Self::new(ButtonContent::Icon(icon))
}
@@ -652,15 +671,22 @@ impl Component for Button {
fn place(&mut self, bounds: Rect) -> Rect {
self.area = bounds;
- let subtext_start =
- self.baseline_text_height() * 2 + constant::LINE_SPACE + self.baseline_subtext_height();
- if let Some(m) = &mut self.subtext_marquee {
- m.place(
- self.area
+
+ if let ButtonContent::TextAndSubtext { icon, .. } = self.content {
+ let subtext_start = (bounds.height() + self.content_height(bounds.width())) / 2
+ - self.baseline_subtext_height();
+ if let Some(m) = self.subtext_marquee.as_mut() {
+ let mut marquee_area = self
+ .area
.inset(Insets::top(subtext_start))
- .inset(Insets::sides(self.content_offset.x)),
- );
+ .inset(Insets::sides(self.content_offset.x));
+ if icon.is_some() {
+ marquee_area = marquee_area.inset(Insets::right(Self::CONN_ICON_WIDTH));
+ }
+ m.place(marquee_area);
+ }
}
+
self.area
}
@@ -870,6 +896,22 @@ impl ButtonContent {
icon,
}
}
+
+ pub const fn text_and_marquee_subtext(
+ text: TString<'static>,
+ subtext: TString<'static>,
+ subtext_style: &'static TextStyle,
+ icon: Option<(Icon, Color)>,
+ ) -> Self {
+ Self::TextAndSubtext {
+ text,
+ single_line: false,
+ subtext,
+ subtext_style,
+ subtext_is_marquee: true,
+ icon,
+ }
+ }
}
#[derive(PartialEq, Eq, Clone, Copy)]
diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs
index 1bba1637..ee6c14dc 100644
--- a/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs
@@ -257,14 +257,14 @@ enum Subscreen {
// A screen allowing the user to to disconnect a device
DeviceScreen(
- TString<'static>, /* device name */
+ TString<'static>, /* device main text */
bool, /* is the device connected? */
u8, /* index in the list of devices */
u8, /* host info screen index */
),
HostInfoScreen(
- TString<'static>, /* device name */
+ TString<'static>, /* host name */
TString<'static>, /* MAC address */
u8, /* parent screen index */
),
@@ -316,10 +316,7 @@ impl DeviceMenuScreen {
init_submenu_idx: Option<u8>,
backup_failed: bool,
backup_needed: bool,
- paired_devices: heapless::Vec<
- (TString<'static>, Option<TString<'static>>),
- MAX_PAIRED_DEVICES,
- >,
+ paired_devices: Vec<(TString<'static>, Option<[TString<'static>; 2]>), MAX_PAIRED_DEVICES>,
connected_idx: Option<u8>,
pin_enabled: Option<bool>,
auto_lock: Option<[TString<'static>; 2]>,
@@ -362,33 +359,40 @@ impl DeviceMenuScreen {
is_connected.then_some(TR::words__connected.into());
let mut submenu_indices: Vec<u8, MAX_PAIRED_DEVICES> = Vec::new();
- for (i, (mac, name)) in (0u8..).zip(paired_devices.iter()) {
+ for (i, (mac, host_info)) in (0u8..).zip(paired_devices.iter()) {
let connected = connected_idx == Some(i);
- let device_name = if let Some(name) = name { *name } else { *mac };
- // Add the host info subscreen first, so that we can reference it from the device subscreen
- let host_info = screen.add_subscreen(Subscreen::HostInfoScreen(
- device_name,
- *mac,
+
+ // Add the host info subscreen first, so that we can reference it from the
+ // device subscreen
+ let host_name = if let Some([host_name, _app_name]) = host_info {
+ *host_name
+ } else {
+ TR::words__unknown.into()
+ };
+ let info_subscreen = screen.add_subscreen(Subscreen::HostInfoScreen(
+ host_name, *mac,
0, /* dummy value because the device subscreen doesn't exist yet */
));
// Add the device subscreen with the reference to the host info subscreen
- let device = screen.add_subscreen(Subscreen::DeviceScreen(
- device_name,
- connected,
- i,
- host_info,
- ));
+ let text = if let Some([host_name, _]) = host_info {
+ *host_name
+ } else {
+ *mac
+ };
+ let device_subscreen =
+ screen.add_subscreen(Subscreen::DeviceScreen(text, connected, i, info_subscreen));
- // Update the parent index in the host info screen to break the circular dependency
+ // Update the parent index in the host info screen to break the circular
+ // dependency
if let Subscreen::HostInfoScreen(_, _, parent_idx) =
- &mut screen.subscreens[usize::from(host_info)]
+ &mut screen.subscreens[usize::from(info_subscreen)]
{
- *parent_idx = device;
+ *parent_idx = device_subscreen;
} else {
unreachable!();
}
- unwrap!(submenu_indices.push(device));
+ unwrap!(submenu_indices.push(device_subscreen));
}
screen.register_pair_and_connect_menu(paired_devices, submenu_indices, connected_idx);
@@ -425,19 +429,33 @@ impl DeviceMenuScreen {
fn register_pair_and_connect_menu(
&mut self,
- paired_devices: Vec<(TString<'static>, Option<TString<'static>>), MAX_PAIRED_DEVICES>,
+ paired_devices: Vec<(TString<'static>, Option<[TString<'static>; 2]>), MAX_PAIRED_DEVICES>,
submenu_indices: Vec<u8, MAX_PAIRED_DEVICES>,
connected_idx: Option<u8>,
) {
let mut items: Vec<MenuItem, MEDIUM_MENU_ITEMS> = Vec::new();
- for (i, ((mac, name), device)) in (0u8..).zip(paired_devices.iter().zip(submenu_indices)) {
+ for (i, ((mac, host_info), device)) in
+ (0u8..).zip(paired_devices.iter().zip(submenu_indices))
+ {
let connection_status = match connected_idx {
Some(idx) if idx == i => Some(true),
_ => Some(false),
};
- let device_title = if let Some(name) = name { *name } else { *mac };
- let item_device = MenuItem::go_to_subscreen(device_title, device)
- .with_connection_status(connection_status);
+
+ let (text, subtext) = if let Some([host_name, app_name]) = host_info {
+ (*app_name, Some(*host_name))
+ } else {
+ (*mac, None)
+ };
+ let mut item_device =
+ MenuItem::go_to_subscreen(text, device).with_connection_status(connection_status);
+
+ if let Some(subtext) = subtext {
+ item_device = item_device
+ .with_subtext(Some((subtext, None)))
+ .with_subtext_marquee();
+ }
+
items.add(item_device);
}
@@ -835,18 +853,13 @@ impl DeviceMenuScreen {
.with_subtitle(device),
);
}
- Subscreen::HostInfoScreen(name, mac, ..) => {
- let show_name = if name == mac {
- TR::words__unknown.into()
- } else {
- name
- };
+ Subscreen::HostInfoScreen(host_name, mac, ..) => {
*self.active_screen.deref_mut() = ActiveScreen::HostInfo(
TextScreen::new(
Paragraphs::new([
Paragraph::new(&theme::TEXT_MEDIUM_EXTRA_LIGHT, TR::words__name)
.with_bottom_padding(theme::PROP_INNER_SPACING),
- Paragraph::new(&theme::TEXT_MONO_LIGHT, show_name)
+ Paragraph::new(&theme::TEXT_MONO_LIGHT, host_name)
.with_bottom_padding(theme::TEXT_VERTICAL_SPACING),
Paragraph::new(&theme::TEXT_MEDIUM_EXTRA_LIGHT, TR::ble__mac_address)
.with_bottom_padding(theme::PROP_INNER_SPACING),
diff --git a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
index ff0539f3..0febfe38 100644
--- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
@@ -1205,7 +1205,7 @@ impl FirmwareUI for UIEckhart {
backup_failed: bool,
backup_needed: bool,
paired_devices: heapless::Vec<
- (TString<'static>, Option<TString<'static>>),
+ (TString<'static>, Option<[TString<'static>; 2]>),
MAX_PAIRED_DEVICES,
>,
connected_idx: Option<u8>,
diff --git a/core/embed/rust/src/ui/ui_firmware.rs b/core/embed/rust/src/ui/ui_firmware.rs
index 1ba7fd2c..a0515432 100644
--- a/core/embed/rust/src/ui/ui_firmware.rs
+++ b/core/embed/rust/src/ui/ui_firmware.rs
@@ -366,7 +366,7 @@ pub trait FirmwareUI {
backup_failed: bool,
backup_needed: bool,
paired_devices: heapless::Vec<
- (TString<'static>, Option<TString<'static>>),
+ (TString<'static>, Option<[TString<'static>; 2]>),
MAX_PAIRED_DEVICES,
>,
connected_idx: Option<u8>,
diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi
index e31b3b7b..7284e34f 100644
--- a/core/mocks/generated/trezorui_api.pyi
+++ b/core/mocks/generated/trezorui_api.pyi
@@ -629,7 +629,7 @@ def show_device_menu(
init_submenu_idx: int | None,
backup_failed: bool,
backup_needed: bool,
- paired_devices: Iterable[tuple[str, str | None]],
+ paired_devices: Iterable[tuple[str, tuple[str, str] | None]],
connected_idx: int | None,
pin_enabled: bool | None,
auto_lock: tuple[str, str] | None,
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.