diff --git a/src/main.rs b/src/main.rs index ac8d11fc14..cbc3fb63e3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -509,7 +509,7 @@ fn load_to_memory( let size = node.data().size(); - print!(" {}: 0/{} MiB", path, size / MIBI as u64); + print!(" {}: {} 0/{} MiB", path, progress_bar(0, size), size / MIBI as u64); let ptr = os.alloc_zeroed_page_aligned(size as usize); if ptr.is_null() { @@ -520,13 +520,15 @@ fn load_to_memory( let mut i = 0; for chunk in slice.chunks_mut(MIBI) { - print!("\r {}: {}/{} MiB", path, i / MIBI as u64, size / MIBI as u64); + print!("\r {}: {} {}/{} MiB", path, progress_bar(i, size), i / MIBI as u64, size / MIBI as u64); i += tx .read_node_inner(&node, i, chunk) .unwrap_or_else(|err| panic!("Failed to read `{}` file: {}", path, err)) as u64; } - println!("\r {}: {}/{} MiB", path, i / MIBI as u64, size / MIBI as u64); + os.set_text_color(TextColor::Green); + println!("\r {}: {} {}/{} MiB", path, progress_bar(size, size), size / MIBI as u64, size / MIBI as u64); + os.set_text_color(TextColor::Default); if filetype == Filetype::Elf { let magic = &slice[..4]; @@ -586,7 +588,58 @@ fn elf_entry(data: &[u8]) -> (u64, bool) { } } -/// Clear the screen and draw the Red Bear OS bootloader header. +// ASCII wordmark (figlet "standard"), pure ASCII so it renders identically on +// the UEFI Unicode console and the VGA CP437 text console (which truncates each +// char to a byte). Keep this ASCII-only. +const WORDMARK: &[&str] = &[ + " ____ _ ____ ___ ____", + "| _ \\ ___ __| | __ ) ___ __ _ _ __ / _ \\/ ___|", + "| |_) / _ \\/ _` | _ \\ / _ \\/ _` | '__| | | | \\___ \\", + "| _ < __/ (_| | |_) | __/ (_| | | | |_| |___) |", + "|_| \\_\\___|\\__,_|____/ \\___|\\__,_|_| \\___/|____/", +]; +const WORDMARK_W: usize = 53; + +/// Return `text` centered within `width` columns, padded with spaces on both +/// sides to exactly `width` characters (truncated if it does not fit). +fn center_in(width: usize, text: &str) -> String { + let tlen = text.chars().count(); + if tlen >= width { + return text.chars().take(width).collect(); + } + let pad = width - tlen; + let left = pad / 2; + let mut s = String::with_capacity(width); + for _ in 0..left { + s.push(' '); + } + s.push_str(text); + for _ in 0..(pad - left) { + s.push(' '); + } + s +} + +/// A fixed-width ASCII progress bar like `[##### ]` for `cur`/`total`. +fn progress_bar(cur: u64, total: u64) -> String { + const BAR_W: usize = 20; + let filled = if total == 0 { + BAR_W + } else { + ((core::cmp::min(cur, total) as u128 * BAR_W as u128) / total as u128) as usize + }; + let mut s = String::with_capacity(BAR_W + 2); + s.push('['); + for i in 0..BAR_W { + s.push(if i < filled { '#' } else { ' ' }); + } + s.push(']'); + s +} + +/// Clear the screen and draw the Red Bear OS bootloader header: a full-width +/// frame around the centered ASCII wordmark, with a centered version/platform +/// subtitle below. /// /// Called at the start of each phase (filesystem, resolution menu, loading) so /// every phase renders on a clean, consistently-branded screen and no phase @@ -595,13 +648,28 @@ fn elf_entry(data: &[u8]) -> (u64, bool) { /// loading progress was drawn on top of the still-visible menu (e.g. "live:" /// landing on the "Autobooting" line). fn draw_header(os: &impl Os) { + let w = os.text_columns().max(20); os.clear_text(); os.set_text_position(0, 0); + os.set_text_color(TextColor::Red); - println!(" Red Bear OS"); + if w >= WORDMARK_W + 2 { + let inner = w - 2; + let border = format!("+{}+", "=".repeat(inner)); + println!("{}", border); + for line in WORDMARK { + println!("|{}|", center_in(inner, line)); + } + println!("{}", border); + } else { + // Narrow console: skip the wordmark, just center the name. + println!("{}", center_in(w, "RedBear OS")); + } + + os.set_text_color(TextColor::Cyan); + let subtitle = format!("Bootloader {} ({})", env!("CARGO_PKG_VERSION"), os.name()); + println!("{}", center_in(w, &subtitle)); os.set_text_color(TextColor::Default); - println!(" Bootloader {} ({})", env!("CARGO_PKG_VERSION"), os.name()); - println!(" --------------------------------------------"); println!(); } @@ -654,7 +722,7 @@ fn main(os: &impl Os) -> (usize, u64, KernelArgs) { let live_opt = if live { let size = fs.header.size(); - print!(" live: 0/{} MiB", size / MIBI as u64); + print!(" live: {} 0/{} MiB", progress_bar(0, size), size / MIBI as u64); let live_size = match usize::try_from(size) { Ok(live_size) => live_size, @@ -688,14 +756,16 @@ fn main(os: &impl Os) -> (usize, u64, KernelArgs) { if let Some(live) = live { let mut i = 0; for chunk in live.chunks_mut(MIBI) { - print!("\r live: {}/{} MiB", i / MIBI as u64, size / MIBI as u64); + print!("\r live: {} {}/{} MiB", progress_bar(i, size), i / MIBI as u64, size / MIBI as u64); i += unsafe { fs.disk .read_at(fs.block + i / redoxfs::BLOCK_SIZE, chunk) .expect("Failed to read live disk") as u64 }; } - println!("\r live: {}/{} MiB", i / MIBI as u64, size / MIBI as u64); + os.set_text_color(TextColor::Green); + println!("\r live: {} {}/{} MiB", progress_bar(size, size), size / MIBI as u64, size / MIBI as u64); + os.set_text_color(TextColor::Default); println!(" Switching to live disk"); unsafe { @@ -837,6 +907,14 @@ fn main(os: &impl Os) -> (usize, u64, KernelArgs) { env_size = w.i; } + os.set_text_color(TextColor::Green); + println!(); + println!( + "{}", + center_in(os.text_columns().max(20), "Booting RedBear OS...") + ); + os.set_text_color(TextColor::Default); + #[allow(static_mut_refs)] ( page_phys, diff --git a/src/os/bios/mod.rs b/src/os/bios/mod.rs index 1cd0b5575c..1b0a958a11 100644 --- a/src/os/bios/mod.rs +++ b/src/os/bios/mod.rs @@ -267,6 +267,10 @@ impl Os for OsBios { } } + fn text_columns(&self) -> usize { + VGA.lock().width + } + fn clear_text(&self) { let mut vga = VGA.lock(); vga.clear(); diff --git a/src/os/mod.rs b/src/os/mod.rs index 368492a3e6..06bc798c74 100644 --- a/src/os/mod.rs +++ b/src/os/mod.rs @@ -104,6 +104,12 @@ pub trait Os { } fn clear_text(&self); + /// Number of character columns on the text console, used to center and + /// frame the header. Defaults to 80 (the universal VGA/UEFI minimum); + /// backends override with the real value when they can query it. + fn text_columns(&self) -> usize { + 80 + } fn get_text_position(&self) -> (usize, usize); fn set_text_position(&self, x: usize, y: usize); fn set_text_highlight(&self, highlight: bool); diff --git a/src/os/uefi/mod.rs b/src/os/uefi/mod.rs index 670767b75b..4194040e93 100644 --- a/src/os/uefi/mod.rs +++ b/src/os/uefi/mod.rs @@ -377,6 +377,22 @@ impl Os for OsEfi { let _ = status_to_result((self.st.ConsoleOut.ClearScreen)(self.st.ConsoleOut)); } + fn text_columns(&self) -> usize { + let mode = self.st.ConsoleOut.Mode.Mode; + if mode < 0 { + return 80; + } + let mut columns = 0usize; + let mut rows = 0usize; + let status = + (self.st.ConsoleOut.QueryMode)(self.st.ConsoleOut, mode as usize, &mut columns, &mut rows); + if status_to_result(status).is_ok() && columns >= 40 { + columns + } else { + 80 + } + } + fn get_text_position(&self) -> (usize, usize) { ( self.st.ConsoleOut.Mode.CursorColumn as usize,