feat(core): allow external menu on Eckhart `show_info()` layout
What changed, and why it matters
This commit is a routine user-interface feature addition for the Trezor hardware wallet firmware. It adds an optional 'external menu' flag to the 'show_info' screen layout, primarily for a new device design (Eckhart/N4W1). On older layouts, the flag is rejected with a 'not implemented' error. There is no security-relevant change visible in the code.
No security action required. Treat as normal UI feature code review.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change extends the FirmwareUI::show_info trait signature with an external_menu: bool parameter, threading it through the micropython binding and all four layout implementations (Bolt, Caesar, Delizia, Eckhart). Bolt/Caesar/Delizia return Error::NotImplementedError when external_menu is true. Eckhart uses the flag to optionally add a menu button to the header and mark the screen as having an external menu. The mock type stub is updated accordingly. No memory-safety, input-validation, cryptographic, or authorization changes are present.
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/ui_firmware.rscore/embed/rust/src/ui/ui_firmware.rscore/mocks/generated/trezorui_api.pyiInspect captured patch +25 / −2
diff --git a/core/embed/rust/src/ui/api/firmware_micropython.rs b/core/embed/rust/src/ui/api/firmware_micropython.rs
index 74870643..dd6db480 100644
--- a/core/embed/rust/src/ui/api/firmware_micropython.rs
+++ b/core/embed/rust/src/ui/api/firmware_micropython.rs
@@ -1081,8 +1081,9 @@ extern "C" fn new_show_info(n_args: usize, args: *const Obj, kwargs: *mut Map) -
})
.transpose()?;
let time_ms: u32 = kwargs.get_or(Qstr::MP_QSTR_time_ms, 0)?.try_into()?;
+ let external_menu: bool = kwargs.get_or(Qstr::MP_QSTR_external_menu, false)?;
- let obj = ModelUI::show_info(title, description, button, time_ms)?;
+ let obj = ModelUI::show_info(title, description, button, time_ms, external_menu)?;
Ok(obj.into())
};
unsafe { util::try_with_args_and_kwargs(n_args, args, kwargs, block) }
@@ -2022,6 +2023,7 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// description: str = "",
/// button: tuple[str, bool] | None = None,
/// time_ms: int = 0,
+ /// external_menu: bool = False,
/// ) -> LayoutObj[UiResult]:
/// """Info screen."""
Qstr::MP_QSTR_show_info => obj_fn_kw!(0, new_show_info).as_obj(),
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 2cd73aaf..762e61f0 100644
--- a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
@@ -992,7 +992,11 @@ impl FirmwareUI for UIBolt {
description: TString<'static>,
button: Option<(TString<'static>, bool)>,
time_ms: u32,
+ external_menu: bool, // TODO: will eventually replace the internal menu
) -> Result<Gc<LayoutObj>, Error> {
+ if external_menu {
+ return Err(Error::NotImplementedError);
+ }
let button_text = match (button, time_ms) {
// either button or timeout must be set
(None, 0) => return Err(Error::NotImplementedError),
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 cca9e3cf..02178d43 100644
--- a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
@@ -1175,7 +1175,11 @@ impl FirmwareUI for UICaesar {
description: TString<'static>,
_button: Option<(TString<'static>, bool)>,
time_ms: u32,
+ external_menu: bool, // TODO: will eventually replace the internal menu
) -> Result<Gc<LayoutObj>, Error> {
+ if external_menu {
+ return Err(Error::NotImplementedError);
+ }
let content = Frame::new(
title,
Paragraphs::new([Paragraph::new(&theme::TEXT_NORMAL, description)]),
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 9069f2b6..9c1bc6cb 100644
--- a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
@@ -973,7 +973,11 @@ impl FirmwareUI for UIDelizia {
description: TString<'static>,
_button: Option<(TString<'static>, bool)>,
_time_ms: u32,
+ external_menu: bool, // TODO: will eventually replace the internal menu
) -> Result<Gc<LayoutObj>, Error> {
+ if external_menu {
+ return Err(Error::NotImplementedError);
+ }
let content = Paragraphs::new(Paragraph::new(&theme::TEXT_MAIN_GREY_LIGHT, description));
let obj = LayoutObj::new(SwipeUpScreen::new(
Frame::left_aligned(title, SwipeContent::new(content)).with_swipeup_footer(None),
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 7e331736..3daf9fe3 100644
--- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
@@ -1235,6 +1235,7 @@ impl FirmwareUI for UIEckhart {
description: TString<'static>,
button: Option<(TString<'static>, bool)>,
_time_ms: u32,
+ external_menu: bool, // TODO: will eventually replace the internal menu
) -> Result<Gc<LayoutObj>, Error> {
let content = Paragraphs::new(Paragraph::new(&theme::TEXT_REGULAR, description))
.with_placement(LinearPlacement::vertical());
@@ -1242,8 +1243,14 @@ impl FirmwareUI for UIEckhart {
let button = button.map_or_else(Button::empty, |(text, enabled)| {
Button::with_text(text).initially_enabled(enabled)
});
+ let header = Header::new(title);
let screen = TextScreen::new(content)
- .with_header(Header::new(title))
+ .with_header(if external_menu {
+ header.with_menu_button()
+ } else {
+ header
+ })
+ .with_external_menu(external_menu)
.with_action_bar(ActionBar::new_single(button));
let obj = LayoutObj::new(screen)?;
Ok(obj)
diff --git a/core/embed/rust/src/ui/ui_firmware.rs b/core/embed/rust/src/ui/ui_firmware.rs
index 46ecb858..3e21fcce 100644
--- a/core/embed/rust/src/ui/ui_firmware.rs
+++ b/core/embed/rust/src/ui/ui_firmware.rs
@@ -387,6 +387,7 @@ pub trait FirmwareUI {
description: TString<'static>,
button: Option<(TString<'static>, bool)>,
time_ms: u32,
+ external_menu: bool, // TODO: will eventually replace the internal menu
) -> Result<Gc<LayoutObj>, Error>; // TODO: return LayoutMaybeTrace
fn show_info_with_cancel(
diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi
index 8c1d4594..954d60d0 100644
--- a/core/mocks/generated/trezorui_api.pyi
+++ b/core/mocks/generated/trezorui_api.pyi
@@ -693,6 +693,7 @@ def show_info(
description: str = "",
button: tuple[str, bool] | None = None,
time_ms: int = 0,
+ external_menu: bool = False,
) -> LayoutObj[UiResult]:
"""Info screen."""
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.