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) <noreply@anthropic.com>
This commit is contained in:
Red Bear OS
2026-07-25 18:46:31 +09:00
parent d7d719775b
commit 3fd4403ecb
4 changed files with 114 additions and 10 deletions
+88 -10
View File
@@ -509,7 +509,7 @@ fn load_to_memory<O: Os>(
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<O: Os>(
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,
+4
View File
@@ -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();
+6
View File
@@ -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);
+16
View File
@@ -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,