use makeover_layout::{Act, Column, ColumnKind, Priority, Sort, Token, Tone, Width};
use makeover_tui::piece::{self, PieceStyle};
use makeover_tui::table::{self, Cell, Sizing, TableStyle};
use makeover_tui::{Fidelity, Theme};
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::widgets::{StatefulWidget, TableState, Widget};
use std::fmt::Write as _;
const WIDTH: u16 = 84;
fn column(
name: &'static str,
width: Width,
priority: Priority,
kind: ColumnKind,
) -> Column<'static> {
let mut column = Column::new(name);
column.width = width;
column.priority = priority;
column.kind = kind;
column
}
fn main() {
let mut args = std::env::args().skip(1);
let id = args.next().unwrap_or_else(|| "akari-night".into());
let fidelity = match args.next().as_deref() {
Some("16") => Fidelity::Ansi16,
Some("256") => Fidelity::Ansi256,
_ => Fidelity::TrueColor,
};
let dir = makeover::bundled_themes_dir().expect("makeover ships themes");
let colors = makeover::load_theme(&[(dir, false)], &id).expect("bundled theme loads");
let theme = Theme::from_theme(&colors)
.expect("theme resolves")
.for_terminal(fidelity);
let pieces = PieceStyle::from_theme(&theme);
let style = TableStyle::from_theme(&theme);
let mut updated = column(
"Updated",
Width::Content,
Priority::Secondary,
ColumnKind::Date,
);
updated.sortable = true;
updated.sorted = Some(Sort::Descending);
let mut title = column("Title", Width::Fill, Priority::Essential, ColumnKind::Text);
title.sortable = true;
let records = [
column(
"Id",
Width::Content,
Priority::Optional,
ColumnKind::Identifier,
),
title,
updated,
column(
"Status",
Width::Content,
Priority::Secondary,
ColumnKind::Status,
),
column(
"Size",
Width::Content,
Priority::Optional,
ColumnKind::Number,
),
column("", Width::Content, Priority::Essential, ColumnKind::Actions),
];
let status = |label: &str, tone| piece::token(&pieces, label, Token::Badge, tone, false, false);
let actions = || {
let mut line = piece::act(&pieces, &Act::new("Edit"), false);
line.spans.push(" ".into());
line.spans.extend(
piece::act(
&pieces,
&Act {
tone: Tone::Danger,
..Act::new("Delete")
},
false,
)
.spans,
);
line
};
let data = [
(
"a3f9c1",
"Field recordings, volume two",
"2026-09-11",
"published",
Tone::Success,
"1.4 GB",
),
(
"7be204",
"Stems for the October single",
"2026-09-10",
"draft",
Tone::Neutral,
"812 MB",
),
(
"0c55de",
"Liner notes",
"2026-09-08",
"processing",
Tone::Info,
"44 KB",
),
(
"e91a07",
"Live at the Mercury, full set",
"2026-09-02",
"failed",
Tone::Danger,
"3.1 GB",
),
(
"5d0f3b",
"Artwork, square and wide",
"2026-08-29",
"needs review",
Tone::Warning,
"18 MB",
),
(
"b41c88",
"Press kit",
"2026-08-21",
"published",
Tone::Success,
"6 MB",
),
];
let rows: Vec<Vec<Cell<'_>>> = data
.iter()
.map(|(id, name, date, label, tone, size)| {
vec![
Cell::new("Id", *id),
Cell::new("Title", *name),
Cell::new("Updated", *date),
Cell::new("Status", status(label, *tone)).part(makeover_layout::CellPart::Tokens),
Cell::new("Size", *size),
Cell::new("", actions()).part(makeover_layout::CellPart::Actions),
]
})
.collect();
let code_columns = [
column(
"Line",
Width::Content,
Priority::Essential,
ColumnKind::Number,
),
column("Source", Width::Fill, Priority::Essential, ColumnKind::Code),
];
let source = [
"fn main() {",
" let theme = Theme::from_theme(&colors)?;",
" let style = TableStyle::from_theme(&theme);",
" draw(&style);",
"}",
];
let code_rows: Vec<Vec<Cell<'_>>> = source
.iter()
.enumerate()
.map(|(n, line)| {
vec![
Cell::new("Line", format!("{}", n + 1)),
Cell::new("Source", *line),
]
})
.collect();
let sizing = Sizing {
lengths: &[("Title", 16), ("Source", 20)],
fallback: 8,
};
let record_height = u16::try_from(rows.len()).unwrap() + 1;
let code_height = u16::try_from(code_rows.len()).unwrap() + 1;
let height = 1 + record_height + 2 + code_height + 1;
let area = Rect::new(0, 0, WIDTH, height);
let mut buf = Buffer::empty(area);
buf.set_style(
area,
Style::new()
.bg(theme.surface_page)
.fg(theme.content_primary),
);
let inset = |y, h| Rect::new(2, y, WIDTH - 4, h);
let mut state = TableState::default().with_selected(Some(2));
let records_at = inset(1, record_height);
StatefulWidget::render(
table::table(&records, &rows, &sizing, &style, records_at.width),
records_at,
&mut buf,
&mut state,
);
let code_at = inset(1 + record_height + 2, code_height);
Widget::render(
table::table(&code_columns, &code_rows, &sizing, &style, code_at.width),
code_at,
&mut buf,
);
print!(
"{}",
html(&buf, fidelity, theme.surface_page, theme.content_primary)
);
}
fn hex(color: Color, fidelity: Fidelity, fallback: Color) -> String {
let rgb = |c: makeover::Rgb| format!("#{:02x}{:02x}{:02x}", c.r, c.g, c.b);
match color {
Color::Rgb(r, g, b) => format!("#{r:02x}{g:02x}{b:02x}"),
Color::Indexed(i) if fidelity == Fidelity::Ansi16 => rgb(makeover::ANSI_16[usize::from(i)]),
Color::Indexed(i) => rgb(makeover::ANSI_240[usize::from(i) - makeover::ANSI_240_OFFSET]),
_ => hex(fallback, fidelity, fallback),
}
}
fn html(buf: &Buffer, fidelity: Fidelity, page: Color, ink: Color) -> String {
let mut out = String::new();
for y in 0..buf.area.height {
out.push_str("<div class=\"ln\">");
let mut run: Option<(String, String)> = None;
for x in 0..buf.area.width {
let cell = &buf[(x, y)];
let (mut fg, mut bg) = (hex(cell.fg, fidelity, ink), hex(cell.bg, fidelity, page));
if cell.modifier.contains(Modifier::REVERSED) {
std::mem::swap(&mut fg, &mut bg);
}
let half = match cell.symbol() {
"\u{258C}" => Some("to right"),
"\u{2590}" => Some("to left"),
"\u{2580}" => Some("to bottom"),
"\u{2584}" => Some("to top"),
_ => None,
};
if let Some(toward) = half {
if let Some((open, text)) = run.take() {
let _ = write!(out, "<span style=\"{open}\">{text}</span>");
}
let _ = write!(
out,
"<span style=\"background:linear-gradient({toward},{fg} 50%,{bg} 50%)\"> </span>"
);
continue;
}
let mut css = format!("color:{fg};background:{bg}");
if cell.modifier.contains(Modifier::BOLD) {
css.push_str(";font-weight:700");
}
if cell.modifier.contains(Modifier::UNDERLINED) {
css.push_str(";text-decoration:underline");
}
let symbol = match cell.symbol() {
"<" => "<",
">" => ">",
"&" => "&",
s => s,
};
match &mut run {
Some((open, text)) if *open == css => text.push_str(symbol),
_ => {
if let Some((open, text)) = run.take() {
let _ = write!(out, "<span style=\"{open}\">{text}</span>");
}
run = Some((css, symbol.to_owned()));
}
}
}
if let Some((open, text)) = run {
let _ = write!(out, "<span style=\"{open}\">{text}</span>");
}
out.push_str("</div>\n");
}
out
}