2026-09-26 19:31:50 -07:00
|
|
|
use crate::entries::BootableEntry;
|
2026-09-26 20:10:32 -07:00
|
|
|
use crate::menu::{BootMenu, read_key};
|
2026-09-26 19:31:50 -07:00
|
|
|
use anyhow::{Context, Result};
|
|
|
|
|
use core::time::Duration;
|
|
|
|
|
use log::info;
|
|
|
|
|
use uefi::proto::console::text::{Input, Key, ScanCode};
|
|
|
|
|
|
|
|
|
|
/// The characters that can be used to select an entry from keys.
|
|
|
|
|
const ENTRY_NUMBER_TABLE: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
|
|
|
|
|
|
|
|
|
/// A boot menu that prints the entries and selects them by number.
|
|
|
|
|
pub struct BasicMenu;
|
|
|
|
|
|
|
|
|
|
/// Represents the operation that can be performed by the boot menu.
|
|
|
|
|
#[derive(PartialEq, Eq)]
|
|
|
|
|
enum MenuOperation {
|
|
|
|
|
/// The user selected a numbered entry.
|
|
|
|
|
Number(usize),
|
|
|
|
|
/// The user selected the escape key to exit the boot menu.
|
|
|
|
|
Exit,
|
|
|
|
|
/// The user selected the enter key to display the entries again.
|
|
|
|
|
Continue,
|
|
|
|
|
/// Timeout occurred.
|
|
|
|
|
Timeout,
|
|
|
|
|
/// No operation should be performed.
|
|
|
|
|
Nop,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Read a key from the input device with a duration, returning the [MenuOperation] that was
|
|
|
|
|
/// performed.
|
|
|
|
|
fn read(input: &mut Input, timeout: &Duration) -> Result<MenuOperation> {
|
|
|
|
|
// If the timer triggered, the user did not select a numbered entry.
|
2026-09-26 20:10:32 -07:00
|
|
|
let Some(key) = read_key(input, *timeout)? else {
|
2026-09-26 19:31:50 -07:00
|
|
|
return Ok(MenuOperation::Timeout);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match key {
|
|
|
|
|
Key::Printable(c) => {
|
|
|
|
|
// If the key is not ascii, we can't process it.
|
|
|
|
|
if !c.is_ascii() {
|
|
|
|
|
return Ok(MenuOperation::Continue);
|
|
|
|
|
}
|
|
|
|
|
// Convert the key to a char.
|
|
|
|
|
let c: char = c.into();
|
|
|
|
|
// Find the key pressed in the entry number table or continue.
|
|
|
|
|
Ok(ENTRY_NUMBER_TABLE
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|&x| x == c)
|
|
|
|
|
.map(MenuOperation::Number)
|
|
|
|
|
.unwrap_or(MenuOperation::Continue))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The escape key is used to exit the boot menu.
|
|
|
|
|
Key::Special(ScanCode::ESCAPE) => Ok(MenuOperation::Exit),
|
|
|
|
|
|
|
|
|
|
// If the special key is unknown, do nothing.
|
|
|
|
|
Key::Special(_) => Ok(MenuOperation::Nop),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Selects an entry from the list of entries using the boot menu.
|
|
|
|
|
fn select_with_input<'a>(
|
|
|
|
|
input: &mut Input,
|
|
|
|
|
timeout: Duration,
|
|
|
|
|
entries: &'a [BootableEntry],
|
|
|
|
|
) -> Result<&'a BootableEntry> {
|
|
|
|
|
loop {
|
|
|
|
|
// If the timeout is not zero, let's display the boot menu.
|
|
|
|
|
if !timeout.is_zero() {
|
|
|
|
|
// Print all the entries with the number used to select them.
|
|
|
|
|
info!("Boot Menu:");
|
|
|
|
|
for (index, entry) in entries.iter().enumerate() {
|
|
|
|
|
let title = entry.context().stamp(&entry.declaration().title);
|
|
|
|
|
info!(" [{}] {}", index, title);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Read from input until a valid operation is selected.
|
|
|
|
|
let operation = loop {
|
|
|
|
|
// If the timeout is zero, we can exit immediately because there is nothing to do.
|
|
|
|
|
if timeout.is_zero() {
|
|
|
|
|
break MenuOperation::Exit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
info!("Select a boot entry using the number keys.");
|
|
|
|
|
info!("Press Escape to exit and enter to display the entries again.");
|
|
|
|
|
|
|
|
|
|
let operation = read(input, &timeout)?;
|
|
|
|
|
if operation != MenuOperation::Nop {
|
|
|
|
|
break operation;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match operation {
|
|
|
|
|
// Entry was selected by number. If the number is invalid, we continue.
|
|
|
|
|
MenuOperation::Number(index) => {
|
|
|
|
|
let Some(entry) = entries.get(index) else {
|
|
|
|
|
info!("invalid entry number");
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
return Ok(entry);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// When the user exits the boot menu or a timeout occurs, we should
|
|
|
|
|
// boot the default entry, if any.
|
|
|
|
|
MenuOperation::Exit | MenuOperation::Timeout => {
|
|
|
|
|
return entries
|
|
|
|
|
.iter()
|
|
|
|
|
.find(|item| item.is_default())
|
|
|
|
|
.context("no default entry available");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the operation is to continue or nop, we can just run the loop again.
|
|
|
|
|
MenuOperation::Continue | MenuOperation::Nop => {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl BootMenu for BasicMenu {
|
|
|
|
|
/// Selects an entry using the basic menu.
|
|
|
|
|
/// The actual work is done internally in [select_with_input] which is called
|
|
|
|
|
/// within the context of the standard input device.
|
|
|
|
|
fn select<'a>(
|
|
|
|
|
&self,
|
|
|
|
|
timeout: Duration,
|
|
|
|
|
entries: &'a [BootableEntry],
|
|
|
|
|
) -> Result<&'a BootableEntry> {
|
|
|
|
|
uefi::system::with_stdin(move |input| select_with_input(input, timeout, entries))
|
|
|
|
|
}
|
|
|
|
|
}
|