use std::io::IsTerminal;
pub const WORDMARK: &str = r" ██╗ ██╗
╚═╝ ██║ ██╗
██████╗ ██████╗ ██╗ ██████╗ ██║ ██████╗ ███████╗ ██████╗
██╔══██║ ██╔══██╗ ██║ ██╔══██╗ ██║ ██╔══██║ ██╔══██║ ╚═██╔═╝
╚███████╗ ██████╔╝ ██║ ██████╔╝ ██║ ╚███████╗ ██║ ██║ ██║
╚══════╝ ██╔═══╝ ╚═╝ ██╔═══╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝
╚═╝ ╚═╝";
const INDENT: &str = " ";
pub struct Style {
pub bold: &'static str,
pub red: &'static str,
pub blue: &'static str,
pub green: &'static str,
pub yellow: &'static str,
pub dim: &'static str,
pub reset: &'static str,
}
impl Style {
pub fn detect() -> Self {
Self::new(std::io::stdout().is_terminal())
}
pub fn new(terminal: bool) -> Self {
if terminal {
Style {
bold: "\x1b[1m",
red: "\x1b[31m",
blue: "\x1b[34m",
green: "\x1b[32m",
yellow: "\x1b[33m",
dim: "\x1b[2m",
reset: "\x1b[0m",
}
} else {
Style {
bold: "",
red: "",
blue: "",
green: "",
yellow: "",
dim: "",
reset: "",
}
}
}
}
pub fn wordmark() {
let s = Style::detect();
println!();
for line in WORDMARK.lines() {
println!("{INDENT}{}{line}{}", s.blue, s.reset);
}
}
pub fn heading(what: &str, subject: Option<&str>) {
let s = Style::detect();
println!();
match subject {
Some(subject) => println!(
"{INDENT}{b}{what}{r} {d}{subject}{r}",
b = s.bold,
d = s.dim,
r = s.reset
),
None => println!("{INDENT}{b}{what}{r}", b = s.bold, r = s.reset),
}
println!();
}
pub fn item(text: &str) {
let s = Style::detect();
println!("{INDENT} {d}{text}{r}", d = s.dim, r = s.reset);
}
pub fn detail(name: &str, text: &str) {
let s = Style::detect();
println!("{INDENT} {name:<24}{d}{text}{r}", d = s.dim, r = s.reset);
}
pub fn done(text: &str) {
let s = Style::detect();
println!();
println!("{INDENT}{g}✓{r} {text}", g = s.green, r = s.reset);
println!();
}
pub fn fail(text: &str) {
let s = Style::detect();
eprintln!();
eprintln!("{INDENT}{r_}✗{r} {text}", r_ = s.red, r = s.reset);
}
pub fn note(text: &str) {
let s = Style::detect();
println!(
"{INDENT}{y}!{r} {d}{text}{r}",
y = s.yellow,
d = s.dim,
r = s.reset
);
}
pub fn next(steps: &[(String, &str)]) {
let s = Style::detect();
println!("{INDENT}{b}Next{r}", b = s.bold, r = s.reset);
let width = steps
.iter()
.map(|(command, _)| command.chars().count())
.max()
.unwrap_or(0);
for (command, purpose) in steps {
let pad = width - command.chars().count();
println!(
"{INDENT} {command}{} {d}{purpose}{r}",
" ".repeat(pad),
d = s.dim,
r = s.reset
);
}
println!();
}
pub fn links(title: &str, rows: &[(&str, String)]) {
let s = Style::detect();
let lines: Vec<String> = rows
.iter()
.map(|(label, url)| format!("{label:<7}{url}"))
.collect();
let inner = lines
.iter()
.map(|line| line.chars().count())
.chain(std::iter::once(title.chars().count() + 3))
.max()
.unwrap_or(0)
+ 2;
let title_fill = inner - title.chars().count() - 3;
println!(
"{INDENT}{d}┌─{r} {b}{title}{r} {d}{}┐{r}",
"─".repeat(title_fill),
d = s.dim,
b = s.blue,
r = s.reset
);
for (line, (label, url)) in lines.iter().zip(rows) {
let pad = inner - line.chars().count() - 1;
println!(
"{INDENT}{d}│{r} {d}{label:<7}{r}{url}{}{d}│{r}",
" ".repeat(pad),
d = s.dim,
r = s.reset
);
}
println!(
"{INDENT}{d}└{}┘{r}",
"─".repeat(inner),
d = s.dim,
r = s.reset
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_wordmark_fits_an_eighty_column_terminal() {
let widths: Vec<usize> = WORDMARK.lines().map(|l| l.chars().count()).collect();
assert_eq!(
widths.len(),
7,
"ascender row, five body rows, descender row"
);
assert!(widths.iter().all(|w| w + INDENT.len() <= 80), "{widths:?}");
assert!(!WORDMARK.contains(char::is_alphabetic));
}
#[test]
fn a_box_survives_a_title_wider_than_its_rows() {
links("a.io", &[("API", "http://a.io".into())]);
links(
"a-very-long-domain-name.example.test",
&[("API", "http://x".into())],
);
}
#[test]
fn styles_are_empty_when_output_is_not_a_terminal() {
let s = Style::new(false);
assert_eq!(s.bold, "");
assert_eq!(s.red, "");
assert_eq!(s.blue, "");
assert_eq!(s.green, "");
assert_eq!(s.yellow, "");
assert_eq!(s.dim, "");
assert_eq!(s.reset, "");
}
#[test]
fn styles_are_ansi_when_output_is_a_terminal() {
let s = Style::new(true);
assert_eq!(s.bold, "\x1b[1m");
assert_eq!(s.red, "\x1b[31m");
assert_eq!(s.blue, "\x1b[34m");
assert_eq!(s.green, "\x1b[32m");
assert_eq!(s.yellow, "\x1b[33m");
assert_eq!(s.dim, "\x1b[2m");
assert_eq!(s.reset, "\x1b[0m");
}
}