use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use super::brand::{centred, GAP, GLYPHS, SHORT_TAGLINE, TAGLINE, WORDMARK_W};
use super::look::Look;
use super::palette;
use super::render::fit;
use super::text::{width, Theme};
type Px = Option<(u32, Color)>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Letters {
Flat,
Shadow,
Glow,
Outline,
}
#[derive(Debug, Clone, Default)]
pub struct FrontInfo {
pub version: String,
pub model: String,
pub endpoint: String,
pub folder: String,
pub branch: Option<String>,
pub mcp: Vec<(String, String)>,
pub tools: usize,
pub recent: Vec<(String, String)>,
}
#[derive(Debug, Clone, Default)]
pub struct FrontPage {
pub header: Vec<Line<'static>>,
pub above: Vec<Line<'static>>,
pub below: Vec<Line<'static>>,
pub box_left: usize,
pub box_w: usize,
pub under: bool,
pub dock: bool,
pub top: bool,
}
fn ink(hex: u32, ansi: Color, t: &Theme) -> Color {
if !t.color {
return Color::Reset;
}
palette::ink(hex, t.depth, ansi)
}
fn half_blocks(grid: &[Vec<Px>], t: &Theme) -> Vec<Line<'static>> {
let colour = |p: Px| p.map(|(hex, ansi)| ink(hex, ansi, t));
let fg = |c: Color| if c == Color::Reset { Style::default() } else { Style::default().fg(c) };
let w = grid.iter().map(Vec::len).max().unwrap_or(0);
let mut out = vec![];
for pair in grid.chunks(2) {
let mut spans: Vec<Span<'static>> = vec![];
for x in 0..w {
let u = pair[0].get(x).copied().flatten().and_then(|p| colour(Some(p)));
let l = pair.get(1).and_then(|r| r.get(x).copied().flatten()).and_then(|p| colour(Some(p)));
let (text, style) = match (u, l) {
(None, None) => (" ", Style::default()),
(Some(u), None) => ("โ", fg(u)),
(None, Some(l)) => ("โ", fg(l)),
(Some(u), Some(l)) if u == l => ("โ", fg(u)),
(Some(u), Some(l)) => ("โ", fg(u).bg(l)),
};
match spans.last_mut() {
Some(s) if s.style == style => s.content.to_mut().push_str(text),
_ => spans.push(Span::styled(text.to_string(), style)),
}
}
while spans.last().is_some_and(|s| s.content.trim().is_empty() && s.style.bg.is_none()) {
spans.pop();
}
out.push(Line::from(spans));
}
out
}
fn wordmark_grid(look: Letters, light: bool) -> Vec<Vec<Px>> {
let (paper, lime, shadow) = if light {
((0x101214, Color::Black), (0x4a7200, Color::Green), (0xa9c172, Color::Gray))
} else {
((0xf2f0e9, Color::White), (0xc6ff3d, Color::LightGreen), (0x3d5a10, Color::Green))
};
const GLOW: [(u32, Color); 10] = [
(0xf1ffc4, Color::White),
(0xe4ff9a, Color::LightGreen),
(0xd6ff6e, Color::LightGreen),
(0xc6ff3d, Color::LightGreen),
(0xb8f034, Color::LightGreen),
(0xa8de2c, Color::Green),
(0x97c925, Color::Green),
(0x86b31e, Color::Green),
(0x749c18, Color::Green),
(0x628512, Color::Green),
];
let step = 7 + GAP;
let base = 6 * 7 + 5 * GAP;
let (w, h) = if look == Letters::Shadow { (base + 1, 11) } else { (base, 10) };
let mut letter = vec![vec![None::<usize>; w]; h];
for (gi, rows) in GLYPHS.iter().enumerate() {
for (y, row) in rows.iter().enumerate() {
for (x, c) in row.chars().enumerate() {
if c != '.' {
letter[y][gi * step + x] = Some(gi);
}
}
}
}
let at = |x: isize, y: isize| -> Option<usize> {
(x >= 0 && y >= 0).then(|| letter.get(y as usize).and_then(|r| r.get(x as usize)).copied().flatten()).flatten()
};
let mut grid = vec![vec![None; w]; h];
for (y, row) in grid.iter_mut().enumerate() {
for (x, cell) in row.iter_mut().enumerate() {
let (xi, yi) = (x as isize, y as isize);
*cell = match at(xi, yi) {
Some(gi) => match look {
Letters::Glow => Some(GLOW[y.min(9)]),
Letters::Outline => {
let inside =
[(-1, 0), (1, 0), (0, -1), (0, 1)].iter().all(|(dx, dy)| at(xi + dx, yi + dy).is_some());
(!inside).then_some(lime)
}
_ if gi < 3 => Some(paper),
_ => Some(lime),
},
None if look == Letters::Shadow && at(xi - 1, yi - 1).is_some() => Some(shadow),
None => None,
};
}
}
grid
}
fn scaled(grid: &[Vec<Px>], k: usize) -> Vec<Vec<Px>> {
grid.iter()
.flat_map(|row| {
let wide: Vec<Px> = row.iter().flat_map(|p| std::iter::repeat_n(*p, k)).collect();
std::iter::repeat_n(wide, k)
})
.collect()
}
pub fn wordmark(t: &Theme, look: Letters) -> Vec<Line<'static>> {
half_blocks(&wordmark_grid(look, t.light), t)
}
pub fn wordmark_w(look: Letters) -> usize {
WORDMARK_W + usize::from(look == Letters::Shadow)
}
pub const MARK_W: usize = 16;
pub fn mark_art(t: &Theme) -> Vec<Line<'static>> {
super::brand::mark(t, MARK_W)
}
fn line_w(l: &Line) -> usize {
l.spans.iter().map(|s| width(&s.content)).sum()
}
fn centre_block(lines: Vec<Line<'static>>, w: usize, cols: usize) -> Vec<Line<'static>> {
let x = cols.saturating_sub(w) / 2;
lines.into_iter().map(|l| at(x, l)).collect()
}
fn at(x: usize, l: Line<'static>) -> Line<'static> {
let mut spans = vec![Span::raw(" ".repeat(x))];
spans.extend(l.spans);
Line::from(spans)
}
fn beside(x: usize, left: Vec<Line<'static>>, rx: usize, right: Vec<Line<'static>>) -> Vec<Line<'static>> {
let n = left.len().max(right.len());
(0..n)
.map(|i| {
let l = left.get(i).cloned().unwrap_or_default();
let used = x + line_w(&l);
let mut spans = vec![Span::raw(" ".repeat(x))];
spans.extend(l.spans);
if let Some(r) = right.get(i) {
spans.push(Span::raw(" ".repeat(rx.saturating_sub(used))));
spans.extend(r.spans.clone());
}
Line::from(spans)
})
.collect()
}
fn spread(x: usize, left: Vec<Span<'static>>, end: usize, right: Vec<Span<'static>>) -> Line<'static> {
let lw: usize = left.iter().map(|s| width(&s.content)).sum();
let rw: usize = right.iter().map(|s| width(&s.content)).sum();
let mut spans = vec![Span::raw(" ".repeat(x))];
spans.extend(left);
spans.push(Span::raw(" ".repeat(end.saturating_sub(x + lw + rw).max(1))));
spans.extend(right);
Line::from(spans)
}
fn keep_end(s: &str, max: usize) -> String {
if width(s) <= max {
return s.to_string();
}
let mut tail: Vec<char> = vec![];
let mut used = 1;
for c in s.chars().rev() {
let cw = width(&c.to_string());
if used + cw > max {
break;
}
used += cw;
tail.push(c);
}
tail.reverse();
format!("โฆ{}", tail.into_iter().collect::<String>())
}
fn mcp_text(info: &FrontInfo) -> Option<String> {
let on: Vec<&str> = info.mcp.iter().filter(|(_, s)| s == "connected").map(|(n, _)| n.as_str()).collect();
(!on.is_empty()).then(|| on.join(", "))
}
fn resume_spans(t: &Theme, what: &str, ago: &str, room: usize) -> Vec<Span<'static>> {
vec![
Span::styled("/resume", t.accent()),
Span::raw(" "),
Span::styled(fit(what, room.saturating_sub(width(ago) + 14).max(8)), t.dim()),
Span::styled(" ยท ", t.subtle()),
Span::styled(ago.to_string(), t.subtle()),
]
}
fn clip(l: Line<'static>, w: usize) -> Line<'static> {
let mut used = 0;
let mut spans = vec![];
for s in l.spans {
if used >= w {
break;
}
let room = w - used;
if width(&s.content) <= room {
used += width(&s.content);
spans.push(s);
} else {
let mut text = String::new();
for c in s.content.chars() {
let cw = width(&c.to_string());
if used + cw > w {
break;
}
used += cw;
text.push(c);
}
spans.push(Span::styled(text, s.style));
}
}
Line::from(spans)
}
fn boxed(x: usize, w: usize, rows: Vec<Line<'static>>, border: Style) -> Vec<Line<'static>> {
let inner = w.saturating_sub(4);
let mut out = vec![at(x, Line::from(Span::styled(format!("โญ{}โฎ", "โ".repeat(w.saturating_sub(2))), border)))];
for r in rows {
let r = clip(r, inner);
let used = line_w(&r);
let mut spans = vec![Span::raw(" ".repeat(x)), Span::styled("โ ", border)];
spans.extend(r.spans);
spans.push(Span::raw(" ".repeat(inner - used)));
spans.push(Span::styled(" โ", border));
out.push(Line::from(spans));
}
out.push(at(x, Line::from(Span::styled(format!("โฐ{}โฏ", "โ".repeat(w.saturating_sub(2))), border))));
out
}
pub fn front_page(t: &Theme, look: Look, info: &FrontInfo, cols: usize) -> FrontPage {
let need = match look {
Look::Card => 84,
Look::Frame => 0,
Look::Ledger | Look::Status => 80,
Look::Glow | Look::Bare => 0,
};
if cols < need {
return bare(t, info, cols);
}
match look {
Look::Frame => frame(t, info, cols),
Look::Glow => glow(t, info, cols),
Look::Card => mark_card(t, info, cols),
Look::Ledger => ledger(t, info, cols),
Look::Status => status_board(t, info, cols),
Look::Bare => bare(t, info, cols),
}
}
fn small_name(t: &Theme) -> Vec<Span<'static>> {
super::brand::wordmark_text(t).spans
}
fn frame(t: &Theme, info: &FrontInfo, cols: usize) -> FrontPage {
let one = wordmark_w(Letters::Shadow);
let first_run = info.recent.is_empty();
let (mut above, lw) = if cols < one + 4 {
(vec![centred(Line::from(small_name(t)), cols)], 6)
} else {
let k = if first_run && cols >= 2 * one + 4 { 2 } else { 1 };
let word = half_blocks(&scaled(&wordmark_grid(Letters::Shadow, t.light), k), t);
let ww = k * one;
let mark_n = 2 * word.len();
let gap = 2 * k;
let (logo, lw) = if t.color && cols >= ww + gap + mark_n + 4 {
(beside(0, super::brand::mark(t, mark_n), mark_n + gap, word), mark_n + gap + ww)
} else {
(word, ww)
};
(centre_block(logo, lw, cols), lw)
};
let fw = cols.saturating_sub(8).clamp(cols.min(20), 96.max(lw));
let m = (cols - fw) / 2;
let col = m + 4;
let room = fw.saturating_sub(4);
above.push(Line::default());
above.push(centred(Line::from(Span::styled(SHORT_TAGLINE, t.dim())), cols));
above.push(Line::default());
above.push(Line::default());
let mut below = vec![];
match info.recent.first() {
Some((what, ago)) => {
below.push(Line::default());
below.push(at(col, Line::from(resume_spans(t, what, ago, room))));
}
None => {
below.push(Line::default());
below.push(at(
col,
Line::from(vec![
Span::styled("Try ", t.subtle()),
Span::styled("\"explain this project\"", t.dim()),
Span::styled(" ยท ", t.subtle()),
Span::styled("/init", t.accent()),
Span::styled(" writes KODACT.md for it", t.subtle()),
]),
));
}
}
let failed: Vec<&str> = info.mcp.iter().filter(|(_, s)| s == "failed").map(|(n, _)| n.as_str()).collect();
if !failed.is_empty() {
below.push(Line::default());
below.push(at(
col,
Line::from(vec![
Span::styled("โ ", t.warning()),
Span::styled(fit(&failed.join(", "), room.saturating_sub(24)), t.dim()),
Span::styled(" didn't start ยท ", t.subtle()),
Span::styled("/mcp", t.accent()),
]),
));
}
FrontPage { above, below, box_left: m, box_w: fw, under: true, ..Default::default() }
}
fn glow(t: &Theme, info: &FrontInfo, cols: usize) -> FrontPage {
let mut above = vec![];
let ww = wordmark_w(Letters::Glow);
if cols >= ww + 4 {
let spark = |hex: u32| Style::default().fg(ink(hex, Color::Yellow, t));
let rows: [&[(usize, &str, u32)]; 2] = [
&[(9, "ยท", 0xdcff85), (21, "ห", 0xf1ffc4), (33, "ยท", 0xc6ff3d)],
&[(4, "ห", 0xa8de2c), (15, "ยท", 0xdcff85), (27, "ยท", 0xb8f034), (40, "ห", 0xc6ff3d)],
];
for r in rows {
let mut spans = vec![];
let mut x = 0;
for (at_x, ch, hex) in r {
spans.push(Span::raw(" ".repeat(at_x - x)));
spans.push(Span::styled(ch.to_string(), if t.color { spark(*hex) } else { t.subtle() }));
x = at_x + 1;
}
above.push(at(cols.saturating_sub(ww) / 2, Line::from(spans)));
}
above.extend(centre_block(wordmark(t, Letters::Glow), ww, cols));
let n = 40usize;
let seam: Vec<Span<'static>> = (0..n)
.map(|i| {
let d = (i as f32 - n as f32 / 2.0).abs() / (n as f32 / 2.0);
let hex = if d < 0.2 {
0xf1ffc4
} else if d < 0.5 {
0xc6ff3d
} else if d < 0.8 {
0x749c18
} else {
0x2a3a0c
};
Span::styled("โ", if t.color { Style::default().fg(ink(hex, Color::Yellow, t)) } else { t.subtle() })
})
.collect();
above.push(centred(Line::from(seam), cols));
} else {
above.push(centred(Line::from(small_name(t)), cols));
}
above.push(Line::default());
let tagline = Line::from(Span::styled(TAGLINE, t.dim()));
above.extend(
super::text::wrap(vec![tagline], cols.saturating_sub(4).max(8) as u16).into_iter().map(|l| centred(l, cols)),
);
above.push(Line::default());
above.push(Line::default());
let bw = cols.saturating_sub(8).min(72);
let mut below = vec![];
if let Some((what, ago)) = info.recent.first() {
below.push(Line::default());
below.push(centred(Line::from(resume_spans(t, what, ago, cols)), cols));
}
FrontPage { above, below, box_left: (cols - bw) / 2, box_w: bw, under: true, ..Default::default() }
}
fn mark_card(t: &Theme, info: &FrontInfo, cols: usize) -> FrontPage {
let cw = cols.saturating_sub(8).min(86);
let x = (cols - cw) / 2;
let border = t.hairline();
let inner = cw - 4;
let mut rows = vec![];
rows.push(spread(
0,
vec![Span::styled("kodact", t.accent()), Span::styled(format!(" v{}", info.version), t.subtle())],
inner,
vec![Span::styled("โ ", t.success()), Span::styled("ready", t.dim())],
));
rows.push(Line::from(Span::styled("โ".repeat(inner), border)));
rows.push(Line::default());
let mut right = wordmark(t, Letters::Flat);
right.push(Line::default());
let tag = if inner >= MARK_W + 5 + width(TAGLINE) { TAGLINE } else { "code, then act" };
right.push(Line::from(Span::styled(tag, t.dim())));
rows.extend(beside(1, mark_art(t), MARK_W + 5, right));
rows.push(Line::default());
let kv = |k: &str, v: String, d: String| {
Line::from(vec![Span::styled(format!(" {k:<8}"), t.subtle()), Span::raw(v), Span::styled(d, t.subtle())])
};
rows.push(kv("model", info.model.clone(), format!(" on {}", info.endpoint)));
let tools = match mcp_text(info) {
Some(n) => format!(" ยท {n}"),
None => String::new(),
};
rows.push(kv("tools", format!("{} built in", info.tools), tools));
let branch = info.branch.as_ref().map(|b| format!(" ยท {b}")).unwrap_or_default();
rows.push(kv("folder", keep_end(&info.folder, inner.saturating_sub(30)), branch));
let mut below = vec![];
if let Some((what, ago)) = info.recent.first() {
below.push(Line::default());
below.push(centred(Line::from(resume_spans(t, what, ago, cols)), cols));
}
FrontPage { above: boxed(x, cw, rows, border), below, box_left: x, box_w: cw, ..Default::default() }
}
fn ledger(t: &Theme, info: &FrontInfo, cols: usize) -> FrontPage {
let m = 8;
let end = cols - m;
let mut header = vec![];
let side = vec![
Line::default(),
Line::from(Span::styled(format!("v{}", info.version), t.subtle())),
Line::from(Span::styled(info.model.clone(), t.dim())),
Line::from(Span::styled(info.endpoint.clone(), t.subtle())),
];
let sw = side.iter().map(line_w).max().unwrap_or(0);
header.extend(beside(m, wordmark(t, Letters::Outline), end - sw, side));
header.push(Line::default());
header.push(at(m, Line::from(Span::styled("โ".repeat(end - m), t.hairline()))));
header.push(Line::default());
let half = m + (end - m) / 2 + 4;
let mut left = vec![Line::from(Span::styled("Pick up where you left off", t.subtle())), Line::default()];
if info.recent.is_empty() {
left.push(Line::from(Span::styled("Nothing yet in this folder.", t.dim())));
}
for (i, (what, ago)) in info.recent.iter().take(3).enumerate() {
left.push(Line::from(vec![
Span::styled(format!("{:<4}", i + 1), t.accent()),
Span::raw(fit(what, half.saturating_sub(m + 20))),
Span::styled(format!(" {ago}"), t.subtle()),
]));
}
let mut right = vec![
Line::from(Span::styled("This folder", t.subtle())),
Line::default(),
Line::from(keep_end(&info.folder, end.saturating_sub(half))),
];
if let Some(b) = &info.branch {
right.push(Line::from(Span::styled(format!("โ {b}"), t.dim())));
}
if let Some(n) = mcp_text(info) {
right.push(Line::from(vec![Span::styled("โ ", t.success()), Span::styled(n, t.dim())]));
}
header.extend(beside(m, left, half, right));
FrontPage { above: header, box_left: 2, box_w: cols - 4, dock: true, ..Default::default() }
}
fn status_board(t: &Theme, info: &FrontInfo, cols: usize) -> FrontPage {
let total = cols.saturating_sub(8).min(96);
let x = (cols - total) / 2;
let mut above = centre_block(wordmark(t, Letters::Shadow), wordmark_w(Letters::Shadow), cols);
above.push(Line::default());
above.push(Line::default());
let tw = (total - 4) / 3;
let tile = |k: &str, v: String, d: String| -> Vec<Line<'static>> {
boxed(
0,
tw,
vec![
Line::from(Span::styled(k.to_string(), t.accent())),
Line::from(fit(&v, tw - 4)),
Line::from(Span::styled(fit(&d, tw - 4), t.subtle())),
],
t.hairline(),
)
};
let tiles = [
tile("Model", info.model.clone(), info.endpoint.clone()),
tile(
"Tools",
format!("{} built in", info.tools),
mcp_text(info).map(|n| format!("{n} over MCP")).unwrap_or_else(|| "no MCP servers".into()),
),
tile("Folder", keep_end(&info.folder, tw - 4), info.branch.clone().unwrap_or_default()),
];
for r in 0..tiles[0].len() {
let mut spans = vec![Span::raw(" ".repeat(x))];
for (i, tl) in tiles.iter().enumerate() {
if i > 0 {
spans.push(Span::raw(" "));
}
spans.extend(tl[r].spans.clone());
}
above.push(Line::from(spans));
}
above.push(Line::default());
let mut below = vec![];
if let Some((what, ago)) = info.recent.first() {
below.push(Line::default());
below.push(centred(Line::from(resume_spans(t, what, ago, cols)), cols));
}
FrontPage { above, below, box_left: x, box_w: total, ..Default::default() }
}
fn bare(t: &Theme, info: &FrontInfo, cols: usize) -> FrontPage {
let where_ = format!("{} ยท {}", info.model, info.endpoint);
let right = if cols >= 2 + 9 + 2 + width(&info.version) + 4 + width(&where_) + 2 {
vec![Span::styled(where_, t.subtle())]
} else {
vec![]
};
let under = right.is_empty();
let header = vec![
Line::default(),
spread(
2,
{
let mut v = small_name(t);
v.push(Span::styled(format!(" {}", info.version), t.subtle()));
v
},
cols.saturating_sub(2),
right,
),
];
let bw = cols.saturating_sub(4).min(72);
let x = (cols - bw) / 2;
let above = vec![centred(Line::from(Span::styled("What shall we build today?", t.bold())), cols), Line::default()];
let mut below = vec![];
if !info.recent.is_empty() {
below.push(Line::default());
for (what, ago) in info.recent.iter().take(3) {
below.push(spread(
x + 2,
vec![Span::styled(fit(what, bw.saturating_sub(20)), t.dim())],
x + bw - 2,
vec![Span::styled(ago.clone(), t.subtle())],
));
}
}
FrontPage { header, above, below, box_left: x, box_w: bw, under, ..Default::default() }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tui::palette::Depth;
fn plain(lines: &[Line]) -> Vec<String> {
lines.iter().map(crate::tui::text::plain).collect()
}
const DARK: Theme = Theme { color: true, light: false, accent: None, depth: Depth::True };
fn info() -> FrontInfo {
FrontInfo {
version: "0.1.0".into(),
model: "stand-in".into(),
endpoint: "127.0.0.1:11434".into(),
folder: "~/acme/shop".into(),
branch: Some("fix/cart-rounding".into()),
mcp: vec![("jira".into(), "connected".into()), ("figma".into(), "failed".into())],
tools: 18,
recent: vec![("checkout rounding".into(), "2h ago".into())],
}
}
#[test]
fn the_wordmark_has_four_looks_and_the_mark_is_whole() {
assert_eq!(wordmark(&DARK, Letters::Flat).len(), 5);
assert_eq!(wordmark(&DARK, Letters::Shadow).len(), 6, "the shadow adds a pixel row");
let heat = wordmark(&DARK, Letters::Glow);
let fg = |l: &Line| {
l.spans.iter().find(|s| s.content.contains('โ') || s.content.contains('โ')).and_then(|s| s.style.fg)
};
assert_ne!(fg(&heat[0]), fg(&heat[4]), "hot at the top, cooler at the foot");
let outline = plain(&wordmark(&DARK, Letters::Outline));
assert!(outline.iter().any(|l| l.contains(' ')), "hollow letters");
let a = mark_art(&DARK);
assert_eq!(a.len(), 8);
assert!(a.iter().all(|l| line_w(l) <= MARK_W));
}
#[test]
fn every_style_fits_and_says_what_the_session_has() {
for look in Look::ALL {
for cols in [140, 100, 84, 60, 40] {
let p = front_page(&DARK, look, &info(), cols);
let rows: Vec<&Line> = p.header.iter().chain(&p.above).chain(&p.below).collect();
for l in &rows {
assert!(line_w(l) <= cols, "{look:?} {cols}: {:?}", plain(std::slice::from_ref(*l)));
}
assert!(p.box_left + p.box_w <= cols, "{look:?} {cols}: the input fits");
let text = rows.iter().map(|l| crate::tui::text::plain(l)).collect::<Vec<_>>().join("\n");
assert!(text.contains("stand-in") || p.under, "{look:?} {cols}: the model shows\n{text}");
}
}
let frame = plain(&front_page(&DARK, Look::Frame, &info(), 120).above).join("\n");
assert!(!frame.contains("Welcome to"), "the name speaks for itself");
let first = FrontInfo { recent: vec![], ..info() };
let big = front_page(&DARK, Look::Frame, &first, 140).above;
assert!(big.iter().filter(|l| line_w(l) > 2 * WORDMARK_W).count() >= 10, "twice the size on a first run");
assert!(big.iter().all(|l| line_w(l) <= 140));
let again = front_page(&DARK, Look::Frame, &info(), 140).above;
assert_eq!(big.len() - again.len(), 5, "normal size (6 rows, not 11) once there is history");
let first_below = plain(&front_page(&DARK, Look::Frame, &first, 120).below).join("\n");
assert!(first_below.contains("/init writes KODACT.md"), "a first run says what to try: {first_below}");
let p = front_page(&DARK, Look::Frame, &first, 140);
assert!(p.box_w >= 2 * WORDMARK_W, "{}", p.box_w);
assert!(frame.contains(SHORT_TAGLINE));
for gone in ["Version", "tools ready", "KodAct reads", "~/acme/shop", "jira"] {
assert!(!frame.contains(gone), "{gone}\n{frame}");
}
let below = plain(&front_page(&DARK, Look::Frame, &info(), 120).below).join("\n");
assert!(below.contains("/resume checkout rounding"), "{below}");
assert!(below.contains("figma didn't start ยท /mcp"), "a server that failed says so: {below}");
let ledger = front_page(&DARK, Look::Ledger, &info(), 120);
assert!(ledger.dock && plain(&ledger.above).join("\n").contains("1 checkout rounding"));
let board = plain(&front_page(&DARK, Look::Status, &info(), 120).above).join("\n");
assert!(board.contains("Model") && board.contains("Tools") && board.contains("Folder"));
let narrow = front_page(&DARK, Look::Card, &info(), 60);
assert!(plain(&narrow.header).join("").contains("KodAct"));
let narrow = plain(&front_page(&DARK, Look::Frame, &info(), 50).above).join("\n");
assert!(narrow.contains("KodAct") && narrow.contains(SHORT_TAGLINE), "{narrow}");
assert!(!narrow.contains("What shall we build"), "{narrow}");
let mono = Theme { color: false, ..DARK };
let with = front_page(&DARK, Look::Frame, &info(), 140).above;
let without = front_page(&mono, Look::Frame, &info(), 140).above;
assert!(line_w(&without[3]) < line_w(&with[3]), "no mark without colour");
}
}