diff --git a/src/main.rs b/src/main.rs index 4429d49701..a886e26aba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, + is_more: bool, + } + + let mut entries: Vec = 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 { + 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 { + 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(os: &O) -> (redoxfs::FileSystem, Option<&'static [u8]>) { let attempts = 10; for attempt in 0..=attempts {