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:
Red Bear OS
2026-07-25 18:15:31 +09:00
parent 092784c552
commit f159a47f1c
+226 -157
View File
@@ -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 {