use colored::Colorize as _;
use unicode_width::UnicodeWidthStr as _;
use super::load_and_shade_banner;
use crate::formatters::info_box::{WelcomeData, render_info_box_rows};
pub(crate) const MIN_WIDTH_TWO_PANEL: usize = 90;
const MIN_WIDTH_BOX: usize = 40;
const GUTTER: usize = 3;
const BORDER_R: u8 = 120;
const BORDER_G: u8 = 50;
const BORDER_B: u8 = 10;
const TL: char = '╭';
const TR: char = '╮';
const BL: char = '╰';
const BR: char = '╯';
const HORIZ: char = '─';
const VERT: char = '│';
const DIVIDER_BOTTOM: char = '┴';
pub(crate) fn render_two_panel_banner(
data: &WelcomeData,
term_width: usize,
reconnecting: bool,
) -> Option<String> {
let (image_lines, image_cols) = load_and_shade_banner();
render_two_panel_banner_with_image(data, term_width, reconnecting, &image_lines, image_cols)
}
pub(crate) fn render_two_panel_banner_with_image(
data: &WelcomeData,
term_width: usize,
reconnecting: bool,
image_lines: &[String],
image_cols: usize,
) -> Option<String> {
if term_width < MIN_WIDTH_BOX {
return None;
}
let inner = term_width.saturating_sub(2);
if term_width >= MIN_WIDTH_TWO_PANEL {
let right_col = inner.saturating_sub(image_cols + GUTTER);
if right_col >= 10 {
return Some(render_wide_box(
data,
term_width,
reconnecting,
image_lines,
image_cols,
right_col,
));
}
}
Some(render_narrow_box(
data,
term_width,
reconnecting,
image_lines,
image_cols,
))
}
fn render_title_bar(term_width: usize) -> String {
let version = env!("CARGO_PKG_VERSION");
let label = format!(" Trusty MPM v{version} ");
let label_cols = label.len(); let inner = term_width.saturating_sub(2);
let dashes_left = 4usize.min(inner.saturating_sub(label_cols));
let dashes_right = inner.saturating_sub(dashes_left + label_cols);
let left = tint_border(&format!("{TL}{}", HORIZ.to_string().repeat(dashes_left)));
let right = tint_border(&format!("{}{TR}", HORIZ.to_string().repeat(dashes_right)));
format!("{left}{label}{right}")
}
fn render_wide_box(
data: &WelcomeData,
term_width: usize,
reconnecting: bool,
image_lines: &[String],
left_col: usize,
right_col: usize,
) -> String {
let inner = left_col + GUTTER + right_col;
let right_lines = build_right_lines(data, right_col);
let mut left_lines: Vec<String> = image_lines
.iter()
.map(|row| {
let raw_cols = strip_ansi(row).width();
let pad = left_col.saturating_sub(raw_cols);
format!("{row}{}", " ".repeat(pad))
})
.collect();
if reconnecting {
left_lines.push(" ".repeat(left_col));
let rcon = "Reconnecting...";
let rcon_cols = rcon.width();
let pad_l = (left_col.saturating_sub(rcon_cols)) / 2;
let pad_r = left_col.saturating_sub(rcon_cols + pad_l);
left_lines.push(format!(
"{}{}{}",
" ".repeat(pad_l),
rcon,
" ".repeat(pad_r)
));
}
let height = left_lines.len().max(right_lines.len());
let mut out = String::new();
out.push_str(&render_title_bar(term_width));
out.push('\n');
for i in 0..height {
let left = left_lines
.get(i)
.cloned()
.unwrap_or_else(|| " ".repeat(left_col));
let right = right_lines
.get(i)
.cloned()
.unwrap_or_else(|| " ".repeat(right_col));
out.push_str(&tint_border(&VERT.to_string()));
out.push_str(&left);
out.push_str(&format!(" {} ", tint_border(&VERT.to_string())));
out.push_str(&right);
out.push_str(&tint_border(&VERT.to_string()));
out.push('\n');
}
let bottom = format!(
"{BL}{}{DIVIDER_BOTTOM}{}{BR}",
HORIZ.to_string().repeat(left_col + 1),
HORIZ.to_string().repeat(inner - left_col - 2)
);
out.push_str(&tint_border(&bottom));
out.push('\n');
out
}
fn render_narrow_box(
data: &WelcomeData,
term_width: usize,
reconnecting: bool,
image_lines: &[String],
_image_cols: usize,
) -> String {
let inner = term_width.saturating_sub(2);
let mut out = String::new();
out.push_str(&render_title_bar(term_width));
out.push('\n');
for row in image_lines {
let raw_cols = strip_ansi(row).width();
let pad_l = (inner.saturating_sub(raw_cols)) / 2;
let pad_r = inner.saturating_sub(raw_cols + pad_l);
out.push_str(&tint_border(&VERT.to_string()));
out.push_str(&" ".repeat(pad_l));
out.push_str(row);
out.push_str(&" ".repeat(pad_r));
out.push_str(&tint_border(&VERT.to_string()));
out.push('\n');
}
if reconnecting {
let label = "Reconnecting...";
let label_cols = label.width();
let pad_l = (inner.saturating_sub(label_cols)) / 2;
let pad_r = inner.saturating_sub(label_cols + pad_l);
out.push_str(&tint_border(&VERT.to_string()));
out.push_str(&" ".repeat(pad_l));
out.push_str(label);
out.push_str(&" ".repeat(pad_r));
out.push_str(&tint_border(&VERT.to_string()));
out.push('\n');
}
for row in build_right_lines(data, inner) {
let bare = strip_ansi(&row);
let bare_cols = bare.width();
let pad = inner.saturating_sub(bare_cols);
out.push_str(&tint_border(&VERT.to_string()));
out.push_str(&row);
out.push_str(&" ".repeat(pad));
out.push_str(&tint_border(&VERT.to_string()));
out.push('\n');
}
let bottom = format!("{BL}{}{BR}", HORIZ.to_string().repeat(inner));
out.push_str(&tint_border(&bottom));
out.push('\n');
out
}
fn build_right_lines(data: &WelcomeData, inner_width: usize) -> Vec<String> {
let rows = render_info_box_rows(data);
rows.into_iter()
.map(|row| {
let bare = strip_ansi(&row);
let cols = bare.width();
if cols <= inner_width {
format!("{row}{}", " ".repeat(inner_width - cols))
} else {
truncate_to_cols(row, inner_width)
}
})
.collect()
}
pub(crate) fn strip_ansi(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
if c == '\x1B' {
if chars.peek() == Some(&'[') {
chars.next();
while let Some(&nc) = chars.peek() {
chars.next();
if nc.is_ascii_alphabetic() {
break;
}
}
}
} else {
out.push(c);
}
}
out
}
fn truncate_to_cols(s: String, max_cols: usize) -> String {
if s.width() <= max_cols {
return s;
}
let budget = max_cols.saturating_sub(1);
let mut used = 0usize;
let mut out = String::new();
for ch in s.chars() {
let w = unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1);
if used + w > budget {
break;
}
used += w;
out.push(ch);
}
let pad = max_cols.saturating_sub(used + 1);
format!("{out}\u{2026}{}", " ".repeat(pad))
}
fn tint_border(s: &str) -> String {
s.truecolor(BORDER_R, BORDER_G, BORDER_B).to_string()
}
#[cfg(test)]
pub(crate) mod tests;