bootloader: wire categorized resolution menu + UEFI garbled-text fix
Ports two pieces of menu work that were stranded on the abandoned
`bootloader-detached-pending` lineage (a divergent full-history upstream-1.0.0
rebase that was never merged into this build branch and never pushed):
- 3d41cf7 "categorized resolution menu": replaces the flat area-sorted grid
selector with a categorized list (4K / 2.5K / FullHD / HD / 1024x768) plus
a "More resolutions" sub-menu (select_obscure) for uncommon modes.
- 9a12ee2 "fix garbled text — pad all console lines to consistent width":
the UEFI text console does not clear old characters when a shorter line
overwrites a longer one at the same position, so menu/countdown/live-mode
lines are padded to fixed widths ({:<50}/{:<70}/{:<40}) and fully
overwritten each frame.
Adapted to this branch's select_mode signature and merged with this branch's
best_resolution() fallback: the initial highlight prefers 1280x720, then the
display's EDID-preferred mode, then the largest available — so hardware native
resolution is not lost by the categorized default.
Verified: cargo check passes for both x86_64-unknown-uefi (--bin) and
x86-unknown-none (--lib). Supersedes the menu work on
bootloader-detached-pending, which is being removed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+226
-157
@@ -9,7 +9,6 @@ extern crate uefi_std as std;
|
||||
|
||||
use alloc::{format, string::String, vec::Vec};
|
||||
use core::{
|
||||
cmp,
|
||||
convert::TryFrom,
|
||||
fmt::{self, Write},
|
||||
mem, ptr, slice, str,
|
||||
@@ -119,112 +118,165 @@ fn select_mode(
|
||||
const DEFAULT_HEIGHT: u32 = 720;
|
||||
const AUTOBOOT_SECONDS: usize = 5;
|
||||
|
||||
let mut modes = Vec::new();
|
||||
let mut all_modes = Vec::new();
|
||||
for mode in os.video_modes(output_i) {
|
||||
let mut aspect_w = mode.width;
|
||||
let mut aspect_h = mode.height;
|
||||
for i in 2..cmp::min(aspect_w / 2, aspect_h / 2) {
|
||||
while aspect_w % i == 0 && aspect_h % i == 0 {
|
||||
aspect_w /= i;
|
||||
aspect_h /= i;
|
||||
}
|
||||
}
|
||||
|
||||
modes.push((
|
||||
mode,
|
||||
format!(
|
||||
"{:>4}x{:<4} {:>3}:{:<3}",
|
||||
mode.width, mode.height, aspect_w, aspect_h
|
||||
),
|
||||
));
|
||||
all_modes.push(mode);
|
||||
}
|
||||
|
||||
if modes.is_empty() {
|
||||
if all_modes.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Sort modes by pixel area, reversed
|
||||
modes.sort_by(|a, b| (b.0.width * b.0.height).cmp(&(a.0.width * a.0.height)));
|
||||
all_modes.sort_by(|a, b| (b.width * b.height).cmp(&(a.width * a.height)));
|
||||
|
||||
// Set selected based on Red Bear default resolution first, then best resolution fallback
|
||||
print!("Output {}", output_i);
|
||||
let mut selected = modes.first().map_or(0, |x| x.0.id);
|
||||
let mut selected_from_default = false;
|
||||
for (mode, _text) in modes.iter() {
|
||||
if mode.width == DEFAULT_WIDTH && mode.height == DEFAULT_HEIGHT {
|
||||
selected = mode.id;
|
||||
selected_from_default = true;
|
||||
os.set_text_color(TextColor::Green);
|
||||
print!(", default resolution: {}x{}", DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
os.set_text_color(TextColor::Default);
|
||||
break;
|
||||
fn categorize(width: u32, height: u32) -> u8 {
|
||||
match (width, height) {
|
||||
(w, _) if w >= 3840 => 0,
|
||||
(w, _) if w >= 2560 => 1,
|
||||
(1920, 1080) => 2,
|
||||
(1280, 720) => 3,
|
||||
(1024, 768) => 4,
|
||||
_ => 5,
|
||||
}
|
||||
}
|
||||
if !selected_from_default {
|
||||
if let Some((best_width, best_height)) = os.best_resolution(output_i) {
|
||||
print!(", best resolution: {}x{}", best_width, best_height);
|
||||
for (mode, _text) in modes.iter() {
|
||||
if mode.width == best_width && mode.height == best_height {
|
||||
selected = mode.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let category_labels = ["4K", "2.5K", "FullHD", "HD", "1024x768", "Other"];
|
||||
|
||||
struct MenuEntry {
|
||||
label: String,
|
||||
mode: Option<OsVideoMode>,
|
||||
is_more: bool,
|
||||
}
|
||||
|
||||
let mut entries: Vec<MenuEntry> = Vec::new();
|
||||
|
||||
for cat in 0..5 {
|
||||
let mut cat_modes: Vec<&OsVideoMode> = all_modes
|
||||
.iter()
|
||||
.filter(|m| categorize(m.width, m.height) == cat)
|
||||
.collect();
|
||||
if cat_modes.is_empty() {
|
||||
continue;
|
||||
}
|
||||
cat_modes.sort_by(|a, b| (b.width * b.height).cmp(&(a.width * a.height)));
|
||||
for m in cat_modes {
|
||||
let label = if cat == 3 || cat == 2 || cat == 4 {
|
||||
format!("{}", category_labels[cat as usize])
|
||||
} else {
|
||||
format!("{} ({}x{})", category_labels[cat as usize], m.width, m.height)
|
||||
};
|
||||
entries.push(MenuEntry {
|
||||
label,
|
||||
mode: Some(*m),
|
||||
is_more: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
println!();
|
||||
|
||||
println!("Arrow keys and enter select mode");
|
||||
let live_mode = os.get_text_position();
|
||||
if *live {
|
||||
println!("Press l to disable live mode");
|
||||
} else {
|
||||
println!("Press l to enable live mode");
|
||||
let obscure_count = all_modes
|
||||
.iter()
|
||||
.filter(|m| categorize(m.width, m.height) == 5)
|
||||
.count();
|
||||
if obscure_count > 0 {
|
||||
entries.push(MenuEntry {
|
||||
label: format!("More resolutions ({} more)", obscure_count),
|
||||
mode: None,
|
||||
is_more: true,
|
||||
});
|
||||
}
|
||||
println!("Press e to edit boot environment");
|
||||
println!(
|
||||
"Autobooting default mode in {} seconds (press any key to cancel countdown)",
|
||||
AUTOBOOT_SECONDS
|
||||
);
|
||||
println!();
|
||||
print!(" ");
|
||||
|
||||
let (off_x, off_y) = os.get_text_position();
|
||||
let rows = 12;
|
||||
let mut mode_opt = None;
|
||||
let countdown_y = off_y.saturating_sub(2);
|
||||
if entries.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Pick the initial highlight: prefer the Red Bear default resolution
|
||||
// (1280x720); if the panel does not offer it, fall back to the display's
|
||||
// EDID-preferred mode (best_resolution) so real hardware still boots at its
|
||||
// native resolution; only then fall back to entries[0] (the largest mode).
|
||||
let default_idx = entries
|
||||
.iter()
|
||||
.position(|e| {
|
||||
e.mode
|
||||
.map(|m| m.width == DEFAULT_WIDTH && m.height == DEFAULT_HEIGHT)
|
||||
.unwrap_or(false)
|
||||
})
|
||||
.or_else(|| {
|
||||
os.best_resolution(output_i).and_then(|(bw, bh)| {
|
||||
entries.iter().position(|e| {
|
||||
e.mode.map(|m| m.width == bw && m.height == bh).unwrap_or(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
os.set_text_color(TextColor::Cyan);
|
||||
println!("Output {}", output_i);
|
||||
os.set_text_color(TextColor::Default);
|
||||
|
||||
let selected_mode = |entries: &[MenuEntry], idx: usize| -> Option<OsVideoMode> {
|
||||
entries[idx].mode
|
||||
};
|
||||
|
||||
let mut selected = default_idx;
|
||||
let mut countdown = AUTOBOOT_SECONDS;
|
||||
let mut countdown_active = true;
|
||||
while !modes.is_empty() {
|
||||
let mut row = 0;
|
||||
let mut col = 0;
|
||||
for (mode, text) in modes.iter() {
|
||||
if row >= rows {
|
||||
col += 1;
|
||||
row = 0;
|
||||
}
|
||||
|
||||
os.set_text_position(off_x + col * 20, off_y + row);
|
||||
os.set_text_highlight(mode.id == selected);
|
||||
|
||||
print!("{}", text);
|
||||
|
||||
row += 1;
|
||||
os.set_text_color(TextColor::Yellow);
|
||||
println!(
|
||||
" Autobooting in {} seconds (press any key to cancel)",
|
||||
countdown
|
||||
);
|
||||
os.set_text_color(TextColor::Default);
|
||||
println!(" Use Up/Down to navigate, Enter to select.");
|
||||
let live_y = {
|
||||
if *live {
|
||||
println!(" [L] Live mode: ON");
|
||||
} else {
|
||||
println!(" [L] Live mode: OFF");
|
||||
}
|
||||
os.get_text_position().1
|
||||
};
|
||||
println!(" [E] Edit boot environment");
|
||||
println!();
|
||||
|
||||
let (list_x, list_y) = os.get_text_position();
|
||||
let countdown_y = list_y.saturating_sub(4);
|
||||
|
||||
loop {
|
||||
for (i, entry) in entries.iter().enumerate() {
|
||||
os.set_text_position(list_x, list_y + i);
|
||||
os.set_text_highlight(i == selected);
|
||||
let marker = if i == selected { ">" } else { " " };
|
||||
let default_tag = if entries[i]
|
||||
.mode
|
||||
.map(|m| m.width == DEFAULT_WIDTH && m.height == DEFAULT_HEIGHT)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
" [DEFAULT]"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
// Pad to a fixed width: the UEFI text console does not clear old
|
||||
// characters when a shorter line overwrites a longer one at the
|
||||
// same position, so without padding the menu garbles as the
|
||||
// highlight/marker/tag change length between frames.
|
||||
print!("{:<50}", format!("{} {}{}", marker, entry.label, default_tag));
|
||||
}
|
||||
os.set_text_highlight(false);
|
||||
|
||||
os.set_text_position(0, countdown_y);
|
||||
os.set_text_highlight(false);
|
||||
if countdown_active {
|
||||
os.set_text_color(TextColor::Yellow);
|
||||
println!(
|
||||
"Autobooting default mode in {} seconds (press any key to cancel countdown)",
|
||||
let msg = format!(
|
||||
" Autobooting in {} seconds (press any key to cancel)",
|
||||
countdown
|
||||
);
|
||||
print!("{:<70}", msg);
|
||||
os.set_text_color(TextColor::Default);
|
||||
} else {
|
||||
println!("Manual mode selection active. Press Enter to boot selected mode. ");
|
||||
print!("{:<70}", " Manual selection - press Enter to boot");
|
||||
}
|
||||
|
||||
// Read keypress
|
||||
match if countdown_active {
|
||||
os.get_key_timeout(1000)
|
||||
} else {
|
||||
@@ -233,114 +285,131 @@ fn select_mode(
|
||||
OsKey::Timeout => {
|
||||
if countdown_active {
|
||||
if countdown == 0 {
|
||||
if let Some(mode_i) = modes.iter().position(|x| x.0.id == selected) {
|
||||
if let Some((mode, _text)) = modes.get(mode_i) {
|
||||
mode_opt = Some(*mode);
|
||||
}
|
||||
if let Some(mode) = selected_mode(&entries, selected) {
|
||||
return Some(mode);
|
||||
}
|
||||
break;
|
||||
}
|
||||
countdown = countdown.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
OsKey::Left => {
|
||||
countdown_active = false;
|
||||
if let Some(mut mode_i) = modes.iter().position(|x| x.0.id == selected) {
|
||||
if mode_i < rows {
|
||||
while mode_i < modes.len() {
|
||||
mode_i += rows;
|
||||
}
|
||||
}
|
||||
mode_i -= rows;
|
||||
if let Some(new) = modes.get(mode_i) {
|
||||
selected = new.0.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
OsKey::Right => {
|
||||
countdown_active = false;
|
||||
if let Some(mut mode_i) = modes.iter().position(|x| x.0.id == selected) {
|
||||
mode_i += rows;
|
||||
if mode_i >= modes.len() {
|
||||
mode_i %= rows;
|
||||
}
|
||||
if let Some(new) = modes.get(mode_i) {
|
||||
selected = new.0.id;
|
||||
countdown_active = false;
|
||||
} else {
|
||||
countdown -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
OsKey::Up => {
|
||||
countdown_active = false;
|
||||
if let Some(mut mode_i) = modes.iter().position(|x| x.0.id == selected) {
|
||||
if mode_i % rows == 0 {
|
||||
mode_i += rows;
|
||||
if mode_i > modes.len() {
|
||||
mode_i = modes.len();
|
||||
}
|
||||
}
|
||||
mode_i -= 1;
|
||||
if let Some(new) = modes.get(mode_i) {
|
||||
selected = new.0.id;
|
||||
}
|
||||
if selected > 0 {
|
||||
selected -= 1;
|
||||
}
|
||||
}
|
||||
OsKey::Down => {
|
||||
countdown_active = false;
|
||||
if let Some(mut mode_i) = modes.iter().position(|x| x.0.id == selected) {
|
||||
mode_i += 1;
|
||||
if mode_i % rows == 0 {
|
||||
mode_i -= rows;
|
||||
}
|
||||
if mode_i >= modes.len() {
|
||||
mode_i = mode_i - mode_i % rows;
|
||||
}
|
||||
if let Some(new) = modes.get(mode_i) {
|
||||
selected = new.0.id;
|
||||
}
|
||||
if selected + 1 < entries.len() {
|
||||
selected += 1;
|
||||
}
|
||||
}
|
||||
OsKey::Enter => {
|
||||
countdown_active = false;
|
||||
if let Some(mode_i) = modes.iter().position(|x| x.0.id == selected) {
|
||||
if let Some((mode, _text)) = modes.get(mode_i) {
|
||||
mode_opt = Some(*mode);
|
||||
}
|
||||
if entries[selected].is_more {
|
||||
return select_obscure(os, &all_modes, live, edit_env);
|
||||
}
|
||||
if let Some(mode) = selected_mode(&entries, selected) {
|
||||
return Some(mode);
|
||||
}
|
||||
break;
|
||||
}
|
||||
OsKey::Char('l') => {
|
||||
countdown_active = false;
|
||||
*live = !*live;
|
||||
os.set_text_position(live_mode.0, live_mode.1);
|
||||
if *live {
|
||||
println!("Press l to disable live mode");
|
||||
os.set_text_position(0, live_y);
|
||||
let msg = if *live {
|
||||
" [L] Live mode: ON"
|
||||
} else {
|
||||
println!("Press l to enable live mode");
|
||||
}
|
||||
" [L] Live mode: OFF"
|
||||
};
|
||||
print!("{:<40}", msg);
|
||||
}
|
||||
OsKey::Char('e') => {
|
||||
countdown_active = false;
|
||||
if let Some(mode_i) = modes.iter().position(|x| x.0.id == selected) {
|
||||
if let Some((mode, _text)) = modes.get(mode_i) {
|
||||
*edit_env = true;
|
||||
mode_opt = Some(*mode);
|
||||
}
|
||||
if let Some(mode) = selected_mode(&entries, selected) {
|
||||
*edit_env = true;
|
||||
return Some(mode);
|
||||
}
|
||||
break;
|
||||
}
|
||||
OsKey::Other | OsKey::Backspace | OsKey::Delete | OsKey::Char(_) => {
|
||||
_ => {
|
||||
countdown_active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
os.set_text_position(0, off_y + rows);
|
||||
os.set_text_highlight(false);
|
||||
println!();
|
||||
|
||||
mode_opt
|
||||
}
|
||||
|
||||
fn select_obscure(
|
||||
os: &impl Os,
|
||||
all_modes: &[OsVideoMode],
|
||||
live: &mut bool,
|
||||
edit_env: &mut bool,
|
||||
) -> Option<OsVideoMode> {
|
||||
let obscure: Vec<&OsVideoMode> = all_modes
|
||||
.iter()
|
||||
.filter(|m| match (m.width, m.height) {
|
||||
(w, _) if w >= 3840 => false,
|
||||
(w, _) if w >= 2560 => false,
|
||||
(1920, 1080) => false,
|
||||
(1280, 720) => false,
|
||||
(1024, 768) => false,
|
||||
_ => true,
|
||||
})
|
||||
.collect();
|
||||
|
||||
if obscure.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
os.clear_text();
|
||||
os.set_text_color(TextColor::Cyan);
|
||||
println!("More Resolutions");
|
||||
os.set_text_color(TextColor::Default);
|
||||
println!(" Use Up/Down to navigate, Enter to select, Backspace to go back");
|
||||
println!();
|
||||
|
||||
let (_, list_y) = os.get_text_position();
|
||||
let mut selected = 0;
|
||||
|
||||
loop {
|
||||
for (i, mode) in obscure.iter().enumerate() {
|
||||
os.set_text_position(0, list_y + i);
|
||||
os.set_text_highlight(i == selected);
|
||||
let marker = if i == selected { ">" } else { " " };
|
||||
// Pad to a fixed width (see select_mode): prevents UEFI console
|
||||
// garbling as the highlight marker moves between rows.
|
||||
print!("{:<40}", format!("{} {:>4}x{:<4}", marker, mode.width, mode.height));
|
||||
}
|
||||
os.set_text_highlight(false);
|
||||
|
||||
match os.get_key() {
|
||||
OsKey::Up => {
|
||||
if selected > 0 {
|
||||
selected -= 1;
|
||||
}
|
||||
}
|
||||
OsKey::Down => {
|
||||
if selected + 1 < obscure.len() {
|
||||
selected += 1;
|
||||
}
|
||||
}
|
||||
OsKey::Backspace => {
|
||||
return None;
|
||||
}
|
||||
OsKey::Enter => {
|
||||
return Some(*obscure[selected]);
|
||||
}
|
||||
OsKey::Char('l') => {
|
||||
*live = !*live;
|
||||
}
|
||||
OsKey::Char('e') => {
|
||||
*edit_env = true;
|
||||
return Some(*obscure[selected]);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn redoxfs<O: Os>(os: &O) -> (redoxfs::FileSystem<O::D>, Option<&'static [u8]>) {
|
||||
let attempts = 10;
|
||||
for attempt in 0..=attempts {
|
||||
|
||||
Reference in New Issue
Block a user