fix(boot): make the graphical menu mouse opt-in

Add menu-styles.graphical.enable-mouse, which is off by default. Without it, or without a pointing device, the cursor is not drawn and the hint only mentions the keyboard.
This commit is contained in:
alex authored and Alex Zenla committed 2026-10-04 14:03:34 -07:00
1 parent ffbfcbc1f0
commit 9ac55ada50
5 files changed
+73 -9

No files matched your search

+11 -1
View File
@@ -130,7 +130,7 @@ $ sprout.efi --boot="Boot Xen"
$ sprout.efi --autoconfigure $ sprout.efi --autoconfigure
# Use the basic boot menu instead of the simple one. # Use the basic boot menu instead of the simple one.
$ sprout.efi --menu-style=basic $ sprout.efi --menu-style=basic
# Use the graphical boot menu, which can be used with the mouse. # Use the graphical boot menu.
$ sprout.efi --menu-style=graphical $ sprout.efi --menu-style=graphical
# Show the boot menu for 10 seconds before booting the default entry. # Show the boot menu for 10 seconds before booting the default entry.
$ sprout.efi --menu-timeout=10 $ sprout.efi --menu-timeout=10
@@ -142,6 +142,16 @@ $ sprout.efi --retain-boot-console
$ sprout.efi --bls-strict-mode $ sprout.efi --bls-strict-mode
``` ```
### Graphical Menu
The graphical menu selects entries with the keyboard. The mouse is off by default. To use it, enable it in
`sprout.toml`. The cursor is only shown when the mouse is enabled and the firmware has a pointing device.
```toml
[menu-styles.graphical]
enable-mouse = true
```
### Boot Linux from ESP ### Boot Linux from ESP
```toml ```toml
+11 -1
View File
@@ -630,6 +630,9 @@ fn run(reboot_on_error: &mut bool) -> Result<()> {
.menu_style .menu_style
.unwrap_or(config.options.menu_style); .unwrap_or(config.options.menu_style);
// The graphical menu only uses the mouse when it is enabled in the configuration.
let graphical_mouse = config.menu_styles.graphical.enable_mouse;
// Find the forced boot entry, unless the boot menu is forced. // Find the forced boot entry, unless the boot menu is forced.
// The oneshot entry from the bootloader interface is forced by id, and it is used instead of // The oneshot entry from the bootloader interface is forced by id, and it is used instead of
// --boot. If the forced boot entry can't be found, such as when it was removed, // --boot. If the forced boot entry can't be found, such as when it was removed,
@@ -664,7 +667,14 @@ fn run(reboot_on_error: &mut bool) -> Result<()> {
let entry = match forced_entry { let entry = match forced_entry {
Some(entry) => entry, Some(entry) => entry,
// Delegate to the menu to select an entry to boot. // Delegate to the menu to select an entry to boot.
None => menu::select(&timer, menu_timeout, menu_disabled, menu_style, &entries) None => menu::select(
&timer,
menu_timeout,
menu_disabled,
menu_style,
graphical_mouse,
&entries,
)
.context("unable to select entry via boot menu")?, .context("unable to select entry via boot menu")?,
}; };
+7 -2
View File
@@ -22,7 +22,7 @@ pub mod basic;
/// font: The bitmap font that the graphical menu draws text with. /// font: The bitmap font that the graphical menu draws text with.
mod font; mod font;
/// graphical: A menu drawn on the graphics output that is used with the keyboard or the mouse. /// graphical: A menu drawn on the graphics output that is used with the keyboard, or the mouse if enabled.
pub mod graphical; pub mod graphical;
/// logo: The Sprout logo that the graphical menu draws in a corner. /// logo: The Sprout logo that the graphical menu draws in a corner.
@@ -177,6 +177,7 @@ pub fn select<'live>(
timeout: Option<Duration>, timeout: Option<Duration>,
menu_disabled: bool, menu_disabled: bool,
style: MenuStyle, style: MenuStyle,
graphical_mouse: bool,
entries: &'live [BootableEntry], entries: &'live [BootableEntry],
) -> Result<&'live BootableEntry> { ) -> Result<&'live BootableEntry> {
// A hidden menu gives the user a moment to ask for the menu with a key press. // A hidden menu gives the user a moment to ask for the menu with a key press.
@@ -208,7 +209,11 @@ pub fn select<'live>(
// The graphical menu needs a graphics output, which not every machine has. // The graphical menu needs a graphics output, which not every machine has.
// If it fails, the simple menu is used instead, which falls back to the basic menu. // If it fails, the simple menu is used instead, which falls back to the basic menu.
MenuStyle::Graphical => GraphicalMenu.select(timeout, entries).or_else(|error| { MenuStyle::Graphical => GraphicalMenu {
enable_mouse: graphical_mouse,
}
.select(timeout, entries)
.or_else(|error| {
warn!( warn!(
"unable to show the graphical boot menu, using the simple menu: {:#}", "unable to show the graphical boot menu, using the simple menu: {:#}",
error error
+12 -5
View File
@@ -29,7 +29,10 @@ const PIXELS_PER_MILLIMETER: i64 = 8;
const TITLE: &str = "sprout"; const TITLE: &str = "sprout";
/// The hint shown once the countdown has been stopped. /// The hint shown once the countdown has been stopped.
const HINT: &str = "Pick an entry with the arrow keys or the mouse."; const HINT: &str = "Pick an entry with the arrow keys.";
/// The hint shown instead of [HINT] when the mouse is in use.
const HINT_MOUSE: &str = "Pick an entry with the arrow keys or the mouse.";
/// The color of the top of the background, which fades into [BACKGROUND_BOTTOM]. /// The color of the top of the background, which fades into [BACKGROUND_BOTTOM].
const BACKGROUND_TOP: (u8, u8, u8) = (0x3a, 0x1c, 0x4e); const BACKGROUND_TOP: (u8, u8, u8) = (0x3a, 0x1c, 0x4e);
@@ -87,8 +90,11 @@ const CURSOR: [&str; 16] = [
" XXXX ", " XXXX ",
]; ];
/// A graphical boot menu that selects entries with the keyboard or the mouse. /// A graphical boot menu that selects entries with the keyboard, or the mouse if enabled.
pub struct GraphicalMenu; pub struct GraphicalMenu {
/// Whether the mouse is used when the firmware has a pointing device.
pub enable_mouse: bool,
}
/// The pointing devices of the firmware. /// The pointing devices of the firmware.
/// Firmware commonly has both kinds of protocol, even if one has no device behind it. /// Firmware commonly has both kinds of protocol, even if one has no device behind it.
@@ -258,7 +264,7 @@ impl Layout {
.map(|entry| entry.title().chars().count()) .map(|entry| entry.title().chars().count())
.max() .max()
.unwrap_or(0); .unwrap_or(0);
let status = HINT.len().max(STATUS_WIDEST.len()); let status = HINT.len().max(HINT_MOUSE.len()).max(STATUS_WIDEST.len());
let panel_width = ((longest + 4) * cell + padding * 2) let panel_width = ((longest + 4) * cell + padding * 2)
.max(40 * cell) .max(40 * cell)
.max((status + 8) * cell) .max((status + 8) * cell)
@@ -806,6 +812,7 @@ fn run(
loop { loop {
scene.status = match remaining { scene.status = match remaining {
Some(remaining) => format!("Boot in {} s.", remaining.as_millis().div_ceil(1000)), Some(remaining) => format!("Boot in {} s.", remaining.as_millis().div_ceil(1000)),
None if mouse.is_some() => String::from(HINT_MOUSE),
None => String::from(HINT), None => String::from(HINT),
}; };
for screen in screens.iter_mut() { for screen in screens.iter_mut() {
@@ -901,7 +908,7 @@ impl BootMenu for GraphicalMenu {
} }
let mut screens = Screen::open_all(entries)?; let mut screens = Screen::open_all(entries)?;
let mut mouse = Mouse::open(); let mut mouse = self.enable_mouse.then(Mouse::open).flatten();
let result = uefi::system::with_stdin(|input| { let result = uefi::system::with_stdin(|input| {
run(input, mouse.take(), &mut screens, timeout, entries, default) run(input, mouse.take(), &mut screens, timeout, entries, default)
+32
View File
@@ -38,6 +38,9 @@ pub struct RootConfiguration {
/// Default options for Sprout. /// Default options for Sprout.
#[serde(default)] #[serde(default)]
pub options: OptionsConfiguration, pub options: OptionsConfiguration,
/// Settings for each style of boot menu.
#[serde(rename = "menu-styles", default)]
pub menu_styles: MenuStylesConfiguration,
/// Values to be inserted into the root sprout context. /// Values to be inserted into the root sprout context.
#[serde(default)] #[serde(default)]
pub values: BTreeMap<String, String>, pub values: BTreeMap<String, String>,
@@ -99,12 +102,30 @@ pub struct OptionsConfiguration {
pub bls_strict_mode: bool, pub bls_strict_mode: bool,
} }
/// Settings for each style of boot menu.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct MenuStylesConfiguration {
/// Settings for the graphical boot menu.
#[serde(default)]
pub graphical: GraphicalMenuConfiguration,
}
/// Settings for the graphical boot menu.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct GraphicalMenuConfiguration {
/// Enables selecting entries with the mouse, and shows the cursor. When disabled, or when
/// the firmware has no pointing device, entries are selected with the keyboard only.
#[serde(rename = "enable-mouse", default)]
pub enable_mouse: bool,
}
/// The default configuration, which matches an empty configuration file. /// The default configuration, which matches an empty configuration file.
impl Default for RootConfiguration { impl Default for RootConfiguration {
fn default() -> Self { fn default() -> Self {
Self { Self {
version: latest_version(), version: latest_version(),
options: Default::default(), options: Default::default(),
menu_styles: Default::default(),
values: Default::default(), values: Default::default(),
drivers: Default::default(), drivers: Default::default(),
extractors: Default::default(), extractors: Default::default(),
@@ -171,6 +192,17 @@ mod tests {
assert_eq!(default.options.autoconfigure, empty.options.autoconfigure); assert_eq!(default.options.autoconfigure, empty.options.autoconfigure);
} }
#[test]
fn graphical_mouse_is_off_by_default() {
assert!(
!RootConfiguration::default()
.menu_styles
.graphical
.enable_mouse
);
assert!(!empty().menu_styles.graphical.enable_mouse);
}
#[test] #[test]
fn bls_strict_mode_is_off_by_default() { fn bls_strict_mode_is_off_by_default() {
assert!(!RootConfiguration::default().options.bls_strict_mode); assert!(!RootConfiguration::default().options.bls_strict_mode);