From 3fd4403ecb1dadebde69be6fceeb50036049a636 Mon Sep 17 00:00:00 2001 From: Red Bear OS Date: Sat, 25 Jul 2026 18:46:31 +0900 Subject: [PATCH] bootloader: framed centered wordmark, progress bars, color polish Beautification pass on the text UI (all pure ASCII so it renders identically on the UEFI Unicode console and the VGA CP437 console, which truncates chars to bytes): - Os::text_columns(): new trait method to query the real console width. BIOS returns the VGA width; UEFI queries SimpleTextOutput QueryMode for the current mode's columns (both fall back to 80). Enables true centering. - draw_header now draws a full-width frame around a centered figlet "RedBear OS" wordmark, with a centered version/platform subtitle, in brand red/cyan. Falls back to a plain centered name on consoles narrower than the wordmark. - Loading progress lines (live / kernel / initfs) gain a fixed-width ASCII progress bar next to the MiB counters, and turn green on completion. - A centered green "Booting RedBear OS..." closes the loading screen. Verified: cargo check passes for x86_64-unknown-uefi (--bin) and x86-unknown-none (--lib). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/main.rs | 98 +++++++++++++++++++++++++++++++++++++++++----- src/os/bios/mod.rs | 4 ++ src/os/mod.rs | 6 +++ src/os/uefi/mod.rs | 16 ++++++++ 4 files changed, 114 insertions(+), 10 deletions(-) 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,