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> {
if term_width < MIN_WIDTH_BOX {
return None;
}
let (image_lines, image_cols) = load_and_shade_banner();
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 {
use super::*;
use crate::formatters::info_box::{CommitLine, DaemonInfo, WelcomeData};
fn base_data() -> WelcomeData {
WelcomeData {
project: "owner/repo".to_string(),
workspace: "/home/user/projects/repo".to_string(),
user: "alice".to_string(),
reconnecting: false,
session_name: String::new(),
daemon: DaemonInfo::default(),
recent_commits: vec![],
memory_status: "(not detected)".to_string(),
search_status: "(not detected)".to_string(),
review_status: "(not detected)".to_string(),
}
}
fn reconnect_data() -> WelcomeData {
WelcomeData {
reconnecting: true,
session_name: "tmpm-my-proj".to_string(),
..base_data()
}
}
fn data_with_commits() -> WelcomeData {
WelcomeData {
recent_commits: vec![CommitLine {
sha: "abc1234".to_string(),
age: "2h".to_string(),
subject: "fix: something important".to_string(),
}],
..base_data()
}
}
#[test]
fn two_panel_compose_alignment() {
colored::control::set_override(false);
let data = base_data();
let result = render_two_panel_banner(&data, 120, false);
assert!(result.is_some(), "wide terminal should produce a banner");
let out = result.unwrap();
for line in out.lines() {
let bare = strip_ansi(line);
if bare.is_empty() {
continue;
}
let first = bare.chars().next().unwrap_or(' ');
assert!(
matches!(first, '╭' | '╰' | '│'),
"each line must start with a box corner or side: {bare:?}"
);
}
colored::control::unset_override();
}
#[test]
fn single_box_title_bar_contains_version() {
colored::control::set_override(false);
let data = base_data();
let out =
render_two_panel_banner(&data, 120, false).expect("wide terminal produces banner");
let first_line = out.lines().next().unwrap_or("");
let bare = strip_ansi(first_line);
assert!(
bare.contains(env!("CARGO_PKG_VERSION")),
"title bar must contain CARGO_PKG_VERSION: {bare:?}"
);
assert!(
bare.starts_with('╭'),
"title bar must start with ╭: {bare:?}"
);
colored::control::unset_override();
}
#[test]
fn image_clip_correct_rows() {
let (lines, _cols) =
super::super::shade_image(super::super::source::DEFAULT_BANNER_ART, false);
let raw_count = super::super::source::DEFAULT_BANNER_ART.lines().count();
assert_eq!(
lines.len(),
raw_count,
"shade_image must return one row per art line: expected {raw_count}, got {}",
lines.len()
);
assert!(raw_count >= 2, "default art must have at least 2 lines");
}
#[test]
fn image_clip_correct_cols() {
let (lines, expected_cols) =
super::super::shade_image(super::super::source::DEFAULT_BANNER_ART, false);
assert!(expected_cols > 0, "auto-sized cols must be > 0");
for (i, line) in lines.iter().enumerate() {
let bare = strip_ansi(line);
let width = bare.width();
assert_eq!(
width, expected_cols,
"row {i} display width ({width}) must equal auto-sized cols ({expected_cols})"
);
}
}
#[test]
fn image_shading_emits_truecolor() {
let (lines, _) = super::super::shade_image(super::super::source::DEFAULT_BANNER_ART, true);
let any_colored = lines.iter().any(|l| l.contains("\x1B[38;2;"));
assert!(
any_colored,
"shaded art must emit truecolor escapes when color is enabled"
);
}
#[test]
fn image_autosize_uses_art_dimensions() {
let custom_art = "ABC\nDE\nFGHI\n";
let (lines, cols) = super::super::shade_image(custom_art, false);
assert_eq!(cols, 4, "max width of 'FGHI' is 4");
assert_eq!(lines.len(), 3, "three non-empty lines");
for (i, line) in lines.iter().enumerate() {
let bare = strip_ansi(line);
assert_eq!(bare.width(), 4, "row {i} must be padded to 4 display cols");
}
}
#[test]
fn right_col_no_inner_border() {
colored::control::set_override(false);
let data = base_data();
let out = render_two_panel_banner(&data, 120, false).expect("banner");
for line in out.lines() {
let bare = strip_ansi(line);
if bare.starts_with('╭') || bare.starts_with('╰') {
continue;
}
let inner: String = bare.chars().skip(1).collect();
let inner_trimmed = inner.trim_end_matches('│');
for ch in ['╭', '╮', '╰', '╯'] {
assert!(
!inner_trimmed.contains(ch),
"inner border char {ch:?} must not appear inside content rows: {bare:?}"
);
}
}
colored::control::unset_override();
}
#[test]
fn reconnect_label_in_narrow_box() {
colored::control::set_override(false);
let data = reconnect_data();
let out = render_two_panel_banner(&data, 80, true).expect("narrow box");
let bare = strip_ansi(&out);
assert!(
bare.contains("Reconnecting..."),
"narrow box must contain 'Reconnecting...' label: {bare:.200}"
);
colored::control::unset_override();
}
#[test]
fn narrow_box_no_reconnect_label_when_not_reconnecting() {
colored::control::set_override(false);
let data = base_data();
let out = render_two_panel_banner(&data, 80, false).expect("narrow box");
let bare = strip_ansi(&out);
assert!(
!bare.contains("Reconnecting..."),
"normal narrow box must not contain 'Reconnecting...' label"
);
colored::control::unset_override();
}
#[test]
fn two_panel_shorter_panel_padded_to_equal_height() {
let data = data_with_commits();
colored::control::set_override(false);
let out = render_two_panel_banner(&data, 130, false).expect("wide banner");
for line in out.lines() {
let bare = strip_ansi(line);
if bare.starts_with('│') {
assert!(
bare.ends_with('│'),
"content line must end with │: {bare:?}"
);
}
}
colored::control::unset_override();
}
#[test]
fn two_panel_narrow_fallback() {
let data = base_data();
assert!(
render_two_panel_banner(&data, MIN_WIDTH_BOX - 1, false).is_none(),
"very narrow terminal must return None"
);
}
#[test]
fn two_panel_stacked_on_medium_width() {
let data = base_data();
let result = render_two_panel_banner(&data, 80, false);
assert!(
result.is_some(),
"80-col terminal must produce a stacked box (not None)"
);
}
#[test]
fn two_panel_at_threshold_boundary() {
let data = base_data();
let _ = render_two_panel_banner(&data, MIN_WIDTH_TWO_PANEL - 1, false);
let _ = render_two_panel_banner(&data, MIN_WIDTH_TWO_PANEL, false);
}
#[test]
fn two_panel_version_present() {
colored::control::set_override(false);
let data = base_data();
let out = render_two_panel_banner(&data, 120, false).expect("two-panel");
let bare = strip_ansi(&out);
assert!(
bare.contains(env!("CARGO_PKG_VERSION")),
"version must appear in banner: {bare:.100}"
);
colored::control::unset_override();
}
#[test]
fn right_panel_omits_version_line() {
colored::control::set_override(false);
let data = base_data();
let version = env!("CARGO_PKG_VERSION");
let out = render_two_panel_banner(&data, 120, false).expect("wide banner");
let bare = strip_ansi(&out);
let count = bare.matches(version).count();
assert_eq!(
count, 1,
"version string must appear exactly once (title bar only); found {count}"
);
colored::control::unset_override();
}
#[test]
fn two_panel_reconnecting_shows_indicator() {
colored::control::set_override(false);
let data = reconnect_data();
let out = render_two_panel_banner(&data, 120, true).expect("two-panel");
let bare = strip_ansi(&out);
assert!(
bare.contains("Reconnecting..."),
"reconnecting indicator must appear: {bare:.200}"
);
colored::control::unset_override();
}
#[test]
fn strip_ansi_removes_color_codes() {
let colored = "\x1B[38;2;183;65;14mhello\x1B[0m world";
let bare = strip_ansi(colored);
assert_eq!(bare, "hello world");
}
#[test]
fn wide_layout_has_vertical_divider() {
colored::control::set_override(false);
let data = base_data();
let out = render_two_panel_banner(&data, 120, false).expect("wide banner");
let mut checked = 0usize;
for line in out.lines() {
let bare = strip_ansi(line);
if bare.is_empty() {
continue;
}
assert_eq!(bare.width(), 120, "line width mismatch: {bare:?}");
if bare.starts_with('│') {
let chars: Vec<char> = bare.chars().collect();
let inner = &chars[1..chars.len().saturating_sub(1)];
assert!(inner.contains(&'│'), "no interior │ divider in: {bare:?}");
checked += 1;
}
}
assert!(checked > 0, "no content lines found in wide banner");
colored::control::unset_override();
}
}