use ratatui::style::{Color, Style};
use ratatui::text::{Line, Span};
use unicode_width::UnicodeWidthStr;
use crate::config::Theme;
pub const TAGLINES: &[&str] = &[
"Enough Developers Are Making A Markdown Editor",
"Engineered Despite Already Many Available Markdown Editors",
"Enterprise-Driven Architecture for Markdown Authoring/Management System",
"Ergonomic, Dependency-Averse Markdown Authoring Made Easy",
"'Ello, Doesn't Anyone's Mum 'Ave More Eggs",
"Embarrassingly Duplicative, Absolutely Mediocre, Amateur Markdown Editor",
"Exceptionally Derivative And Mostly Awful Markdown Editor",
"Error: Data Already Mangled — Another Markdown Editor",
"Eventually, Developers All Make A Markdown Editor",
"Every Damn Agent's Markdown Annoys Me Endlessly",
"Even Dad And Mom Are Markdown Editing",
"Extremely Disappointed At Many Aggravating Markdown Editors",
"Ego Demands Acrimoniously Making A Markdown Editor",
];
pub const SPINNER_FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
const ART: &[&str] = &[
r",o;",
r" ,H.",
r" .lc.",
r" .l:c.",
r" 'lll-;+",
r" 'llll--:,..",
r" ;OOclll::;+,.",
r" OCCCC;;;;::+,",
r" 'CCCluucc;;;:;",
r" 'CcCllll;;;:::,._",
r" ';cCCCcuuu;;;:;;:',_",
r" 'clllllclluc;;;::'..",
r" 'cclCllluucc;;;:::,,,.._",
r" 'CCllllooocc;;;;;:::::+..",
r" 'CCCccllCcloouucc;;;::::+',",
r" 'CCCccllooooc:ooooucc;;;:++:,.",
r" 'OCCcclccclllllccuuu;:,.",
r" ''c:;:ccc:wq;'''",
];
const ART_STYLE: Style = Style::new().fg(Color::Green);
const TITLE: &str = "e.d.a.m.a.m.e.";
const SUBTITLE: &str = "A Markdown editor";
const AUTHOR: &str = "Created by mijowi";
const TAGLINE_WRAP: usize = 44;
const TAGLINE_ROWS: usize = 2;
pub fn body_lines(
theme: &Theme,
tagline_idx: usize,
spinner_frame: usize,
installed: &str,
release: Option<&str>,
) -> Vec<Line<'static>> {
let tagline = TAGLINES[tagline_idx % TAGLINES.len()];
let installed_row = format!("Installed version: v{installed}");
let release_row = match release {
Some(text) => format!("Current release: {text}"),
None => format!(
"Current release: {}",
SPINNER_FRAMES[spinner_frame % SPINNER_FRAMES.len()]
),
};
let version_width = installed_row.width().max(release_row.width());
let art_width = ART.iter().map(|l| l.width()).max().unwrap_or(0);
let width = TAGLINE_WRAP
.max(art_width)
.max(TITLE.width())
.max(SUBTITLE.width())
.max(version_width)
.max(AUTHOR.width());
let mut out: Vec<Line<'static>> = Vec::new();
for art_line in ART {
out.push(centered(
Span::styled(pad_to((*art_line).to_owned(), art_width), ART_STYLE),
width,
));
}
out.push(Line::raw(""));
out.push(centered(Span::styled(TITLE, theme.h1), width));
out.push(centered(Span::raw(SUBTITLE), width));
out.push(Line::raw(""));
let mut tagline_rows = wrap_words(tagline, TAGLINE_WRAP);
tagline_rows.resize(TAGLINE_ROWS, String::new());
for row in tagline_rows {
out.push(centered(Span::styled(row, theme.modal_description), width));
}
out.push(Line::raw(""));
out.push(centered(
Span::raw(pad_to(installed_row, version_width)),
width,
));
out.push(centered(
Span::raw(pad_to(release_row, version_width)),
width,
));
out.push(Line::raw(""));
out.push(centered(Span::styled(AUTHOR, theme.text_muted()), width));
out
}
fn wrap_words(text: &str, width: usize) -> Vec<String> {
let mut rows: Vec<String> = Vec::new();
let mut current = String::new();
for word in text.split_whitespace() {
if !current.is_empty() && current.width() + 1 + word.width() > width {
rows.push(std::mem::take(&mut current));
}
if !current.is_empty() {
current.push(' ');
}
current.push_str(word);
}
if !current.is_empty() {
rows.push(current);
}
rows
}
fn pad_to(mut s: String, width: usize) -> String {
let pad = width.saturating_sub(s.width());
s.extend(std::iter::repeat_n(' ', pad));
s
}
fn centered(span: Span<'static>, width: usize) -> Line<'static> {
let pad = width.saturating_sub(span.width());
let left = pad / 2;
Line::from(vec![
Span::raw(" ".repeat(left)),
span,
Span::raw(" ".repeat(pad - left)),
])
}
#[cfg(test)]
mod tests {
use super::*;
fn theme() -> &'static Theme {
Box::leak(Box::new(Theme::default()))
}
fn flat(lines: &[Line<'_>]) -> String {
lines
.iter()
.map(|l| {
l.spans
.iter()
.map(|s| s.content.as_ref())
.collect::<String>()
})
.collect::<Vec<_>>()
.join("\n")
}
fn normalized(lines: &[Line<'_>]) -> String {
flat(lines).split_whitespace().collect::<Vec<_>>().join(" ")
}
#[test]
fn pending_release_renders_spinner_frame() {
let body = flat(&body_lines(theme(), 0, 2, "0.1.0", None));
assert!(body.contains(SPINNER_FRAMES[2]), "{body}");
let body = flat(&body_lines(
theme(),
0,
SPINNER_FRAMES.len() + 1,
"0.1.0",
None,
));
assert!(body.contains(SPINNER_FRAMES[1]), "{body}");
}
#[test]
fn tagline_index_selects_and_wraps() {
let body = normalized(&body_lines(theme(), 4, 0, "0.1.0", None));
assert!(body.contains(TAGLINES[4]), "{body}");
let wrapped = normalized(&body_lines(theme(), TAGLINES.len() + 4, 0, "0.1.0", None));
assert!(wrapped.contains(TAGLINES[4]), "{wrapped}");
let long = normalized(&body_lines(theme(), 2, 0, "0.1.0", None));
assert!(long.contains(TAGLINES[2]), "{long}");
}
#[test]
fn every_rotation_keeps_the_same_size() {
let sizes: Vec<(usize, usize)> = (0..TAGLINES.len())
.map(|i| {
let body = body_lines(theme(), i, 0, "0.1.0", None);
(body.iter().map(|l| l.width()).max().unwrap(), body.len())
})
.collect();
assert!(sizes.windows(2).all(|w| w[0] == w[1]), "{sizes:?}");
}
#[test]
fn content_fits_an_80_column_terminal() {
let body = body_lines(theme(), 2, 0, "0.1.0", Some("v10.20.30 (update available)"));
let max = body.iter().map(|l| l.width()).max().unwrap();
assert!(max <= 72, "body width {max} leaves no room for chrome");
}
#[test]
fn art_block_keeps_its_shape_when_centered() {
let body = body_lines(theme(), 0, 0, "0.1.0", None);
let offsets: Vec<usize> = body[..ART.len()]
.iter()
.map(|l| l.spans[0].content.len())
.collect();
assert!(offsets.windows(2).all(|w| w[0] == w[1]), "{offsets:?}");
}
}