#![deny(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]
use std::path::PathBuf;
use std::sync::OnceLock;
mod error;
pub use error::{FigletError, StrictTarget};
#[allow(dead_code)]
mod figfont;
#[allow(dead_code)]
mod header;
#[allow(dead_code)]
mod layout;
#[allow(dead_code)]
mod mode;
#[allow(dead_code)]
mod smush;
#[cfg(feature = "tlf-parser")]
#[allow(dead_code)]
pub mod tlf;
pub mod filter;
pub mod color_depth;
pub use color_depth::ColorDepth;
pub mod export;
pub use layout::{JustifyFlag, JustifyFlags, LayoutFlag, LayoutFlags};
#[cfg(feature = "strict-compat")]
#[allow(dead_code)]
pub mod strict;
#[cfg(feature = "toilet-strict-compat")]
pub mod strict_toilet;
#[cfg(feature = "cli")]
#[allow(dead_code)]
mod cli;
#[cfg(feature = "color")]
#[doc(hidden)]
#[allow(dead_code)]
pub mod color;
#[cfg(feature = "color")]
#[doc(hidden)]
#[allow(dead_code)]
pub mod output;
#[cfg(feature = "terminal-width")]
#[allow(dead_code)]
mod width;
#[cfg(feature = "terminal-width")]
pub fn resolve_width_for(
explicit_w: Option<u32>,
use_t: bool,
columns_env: Option<u32>,
is_tty: bool,
mode: CompatibilityMode,
) -> u32 {
width::resolve_width(explicit_w, use_t, columns_env, is_tty, mode)
}
pub fn resolve_justify_for(flags: &JustifyFlags) -> Justify {
match layout::resolve_justify(flags) {
layout::Justify::Center => Justify::Center,
layout::Justify::Left => Justify::Left,
layout::Justify::Right => Justify::Right,
layout::Justify::FontDefault => Justify::FontDefault,
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CompatibilityMode {
Default,
Strict,
}
impl Default for CompatibilityMode {
fn default() -> Self {
Self::Default
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Font {
Standard,
Slant,
Small,
Big,
Mini,
Banner,
Block,
Bubble,
Digital,
Lean,
Script,
Shadow,
External(PathBuf),
}
impl Font {
pub(crate) fn bundled_name(&self) -> Option<&'static str> {
Some(match self {
Font::Standard => "standard",
Font::Slant => "slant",
Font::Small => "small",
Font::Big => "big",
Font::Mini => "mini",
Font::Banner => "banner",
Font::Block => "block",
Font::Bubble => "bubble",
Font::Digital => "digital",
Font::Lean => "lean",
Font::Script => "script",
Font::Shadow => "shadow",
Font::External(_) => return None,
})
}
}
impl Default for Font {
fn default() -> Self {
Self::Standard
}
}
#[derive(Debug, Clone)]
enum FontSource {
Bundled(Font),
External(PathBuf),
Bytes(Vec<u8>),
}
#[derive(Debug, Clone)]
pub struct FigletBuilder {
source: FontSource,
width: u32,
layout_override: Option<LayoutOverride>,
layout_flags: LayoutFlags,
justify: Option<Justify>,
font_dirs: Vec<PathBuf>,
color_depth: Option<ColorDepth>,
}
#[derive(Debug, Clone, Copy)]
enum LayoutOverride {
Kerning,
FullWidth,
ForceSmush,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Justify {
Center,
Left,
Right,
FontDefault,
}
impl Default for FigletBuilder {
fn default() -> Self {
Self::new()
}
}
impl FigletBuilder {
#[must_use]
pub fn new() -> Self {
Self {
source: FontSource::Bundled(Font::Standard),
width: 80,
layout_override: None,
layout_flags: LayoutFlags::default(),
justify: None,
font_dirs: Vec::new(),
color_depth: None,
}
}
#[must_use]
pub fn font(mut self, font: Font) -> Self {
self.source = match font {
Font::External(path) => FontSource::External(path),
other => FontSource::Bundled(other),
};
self
}
#[must_use]
pub fn font_bytes(mut self, bytes: &[u8]) -> Self {
self.source = FontSource::Bytes(bytes.to_vec());
self
}
#[must_use]
pub fn font_dirs(mut self, dirs: Vec<PathBuf>) -> Self {
self.font_dirs = dirs;
self
}
#[must_use]
pub fn width(mut self, cols: u32) -> Self {
self.width = cols;
self
}
#[must_use]
pub fn kerning(mut self) -> Self {
self.layout_override = Some(LayoutOverride::Kerning);
self
}
#[must_use]
pub fn full_width(mut self) -> Self {
self.layout_override = Some(LayoutOverride::FullWidth);
self
}
#[must_use]
pub fn smush(mut self) -> Self {
self.layout_override = Some(LayoutOverride::ForceSmush);
self
}
#[must_use]
pub fn layout(mut self, flags: LayoutFlags) -> Self {
self.layout_flags = flags;
self
}
#[must_use]
pub fn justify(mut self, j: Justify) -> Self {
self.justify = Some(j);
self
}
#[must_use]
pub fn color_depth(mut self, depth: ColorDepth) -> Self {
self.color_depth = Some(depth);
self
}
pub fn build(self) -> Result<Figlet, FigletError> {
let bytes = match self.source {
FontSource::Bundled(font) => {
let name = font
.bundled_name()
.ok_or(FigletError::Internal("Font::External missed bundled match"))?;
let slice =
figfont::resolve_bundled(name).ok_or_else(|| FigletError::FontNotFound {
name: name.to_owned(),
searched: Vec::new(),
})?;
slice.to_vec()
}
FontSource::External(path) => {
figfont::resolve_font(path.to_string_lossy().as_ref(), &self.font_dirs)?
}
FontSource::Bytes(bytes) => bytes,
};
let font = figfont::parse_bytes(&bytes)?;
let color_depth = self.color_depth.unwrap_or_else(ColorDepth::detect);
Ok(Figlet {
font,
width: self.width,
layout_override: self.layout_override,
layout_flags: self.layout_flags,
justify: self.justify.unwrap_or(Justify::FontDefault),
color_depth,
})
}
pub fn render(self, text: &str) -> Result<Banner, FigletError> {
self.build()?.render(text)
}
}
#[derive(Debug, Clone)]
pub struct Figlet {
font: figfont::FIGfont,
width: u32,
layout_override: Option<LayoutOverride>,
layout_flags: LayoutFlags,
justify: Justify,
color_depth: ColorDepth,
}
impl Figlet {
pub fn render(&self, text: &str) -> Result<Banner, FigletError> {
let layout = self.resolved_layout();
let rows = render_to_rows(&self.font, text, layout, self.width);
let rows = apply_justify(rows, self.justify, self.width, self.font.print_direction);
let rows = strip_hardblanks(rows, self.font.hardblank);
Ok(Banner {
rows,
height: self.font.height,
})
}
fn resolved_layout(&self) -> layout::LayoutMode {
use layout::{LayoutFlag, LayoutFlags, LayoutResolver};
if !self.layout_flags.flags.is_empty() {
return LayoutResolver::resolve(&self.font, &self.layout_flags);
}
let mut flags = LayoutFlags::default();
if let Some(ov) = self.layout_override {
flags.flags.push(match ov {
LayoutOverride::Kerning => LayoutFlag::Kerning,
LayoutOverride::FullWidth => LayoutFlag::FullWidth,
LayoutOverride::ForceSmush => LayoutFlag::ForceSmush,
});
}
LayoutResolver::resolve(&self.font, &flags)
}
#[cfg(feature = "tlf-parser")]
pub fn from_tlf(path: impl AsRef<std::path::Path>) -> Result<Figlet, FigletError> {
let bytes = tlf::read_tlf_file(path.as_ref())?;
Figlet::from_tlf_bytes(&bytes)
}
#[cfg(feature = "tlf-parser")]
pub fn from_tlf_bytes(bytes: &[u8]) -> Result<Figlet, FigletError> {
let tlf_font = tlf::parse_tlf(bytes)?;
let font = tlf_to_figfont(tlf_font);
Ok(Figlet {
font,
width: 80,
layout_override: None,
layout_flags: LayoutFlags::default(),
justify: Justify::FontDefault,
color_depth: ColorDepth::detect(),
})
}
#[must_use]
pub fn color_depth(&self) -> ColorDepth {
self.color_depth
}
pub fn set_color_depth(&mut self, depth: ColorDepth) {
self.color_depth = depth;
}
}
#[cfg(feature = "tlf-parser")]
fn tlf_to_figfont(tf: tlf::TlfFont) -> figfont::FIGfont {
use std::collections::HashMap as Map;
let height = tf.header.height;
let hardblank = tf.header.hardblank;
let baseline = tf.header.baseline;
let max_length = tf.header.max_length;
let mut glyphs: Map<u32, Vec<String>> = Map::with_capacity(tf.glyphs.len());
for (cp, g) in tf.glyphs.into_iter() {
let rows: Vec<String> = g
.rows
.into_iter()
.map(|row| {
let mut s = String::with_capacity(row.cells.len());
for c in row.cells {
s.push(c.ch);
}
s
})
.collect();
glyphs.insert(cp, rows);
}
figfont::FIGfont {
hardblank,
height,
baseline,
max_length,
old_layout: 0,
full_layout: 0,
print_direction: 0,
glyphs,
codetag_count: 0,
}
}
fn render_to_rows(
font: &figfont::FIGfont,
text: &str,
layout: layout::LayoutMode,
width: u32,
) -> Vec<String> {
let height = font.height.max(1) as usize;
if text.is_empty() {
return vec![String::new(); height];
}
let words: Vec<&str> = text.split(' ').collect();
let target_width = width.max(1) as usize;
let mut all_rows: Vec<String> = vec![String::new(); height];
let mut current_rows: Vec<String> = vec![String::new(); height];
let mut current_visual_width: usize = 0;
let mut line_started = false;
for word in &words {
let mut probe = current_rows.clone();
let mut probe_width = current_visual_width;
if line_started {
append_codepoint(&mut probe, &mut probe_width, font, ' ' as u32, layout);
}
append_word(&mut probe, &mut probe_width, font, word, layout);
if probe_width <= target_width || !line_started {
if !line_started && probe_width > target_width {
warn_over_width(word, target_width);
}
current_rows = probe;
current_visual_width = probe_width;
line_started = true;
} else {
for (acc, line) in all_rows.iter_mut().zip(current_rows.iter()) {
if !acc.is_empty() {
acc.push('\n');
}
acc.push_str(line);
}
current_rows = vec![String::new(); height];
current_visual_width = 0;
append_word(
&mut current_rows,
&mut current_visual_width,
font,
word,
layout,
);
if current_visual_width > target_width {
warn_over_width(word, target_width);
}
}
}
if line_started {
for (acc, line) in all_rows.iter_mut().zip(current_rows.iter()) {
if !acc.is_empty() {
acc.push('\n');
}
acc.push_str(line);
}
}
interleave_wrapped(all_rows, height)
}
fn append_word(
rows: &mut [String],
visual_width: &mut usize,
font: &figfont::FIGfont,
word: &str,
layout: layout::LayoutMode,
) {
for ch in word.chars() {
append_codepoint(rows, visual_width, font, ch as u32, layout);
}
}
fn append_codepoint(
rows: &mut [String],
visual_width: &mut usize,
font: &figfont::FIGfont,
cp: u32,
layout: layout::LayoutMode,
) {
let glyph = match figfont::lookup_codepoint(font, cp) {
Some(g) => g,
None => {
warn_missing_codepoint(cp);
match figfont::lookup_codepoint(font, 0) {
Some(g) => g,
None => return,
}
}
};
merge_glyph(rows, visual_width, glyph, layout, font.hardblank);
}
fn merge_glyph(
rows: &mut [String],
visual_width: &mut usize,
glyph: &[String],
layout: layout::LayoutMode,
hardblank: char,
) {
use layout::LayoutMode;
let (rules, allow_smush, allow_kerning_only) = match layout {
LayoutMode::FullWidth => (0u8, false, false),
LayoutMode::Kerning => (0u8, false, true),
LayoutMode::UniversalSmush => (smush::RULE_HORIZONTAL_SMUSHING, true, true),
LayoutMode::RuleSmush(bits) => {
let only_rule_bits = bits & 0b0011_1111;
(only_rule_bits, true, true)
}
LayoutMode::OverlapOnly => (0u8, false, true),
};
let glyph_chars: Vec<Vec<char>> = glyph.iter().map(|s| s.chars().collect()).collect();
let glyph_width = glyph_chars.iter().map(|r| r.len()).max().unwrap_or(0);
if glyph_width == 0 {
return;
}
if !allow_smush && !allow_kerning_only {
for (i, row) in rows.iter_mut().enumerate() {
if let Some(gr) = glyph_chars.get(i) {
for &c in gr {
row.push(c);
}
for _ in gr.len()..glyph_width {
row.push(' ');
}
} else {
for _ in 0..glyph_width {
row.push(' ');
}
}
}
*visual_width += glyph_width;
return;
}
let row_chars: Vec<Vec<char>> = rows.iter().map(|s| s.chars().collect()).collect();
let acc_widths: Vec<usize> = row_chars.iter().map(|r| r.len()).collect();
let acc_min_width = acc_widths.iter().copied().min().unwrap_or(0);
let max_possible = acc_min_width.min(glyph_width);
let mut overlap = 0usize;
'outer: for k in 1..=max_possible {
let mut row_merges: Vec<Vec<char>> = Vec::with_capacity(rows.len());
for (i, acc_row) in row_chars.iter().enumerate() {
let glyph_row = glyph_chars.get(i).cloned().unwrap_or_default();
let mut merged = Vec::with_capacity(k);
for j in 0..k {
let l = acc_row.get(acc_row.len() - k + j).copied().unwrap_or(' ');
let r = glyph_row.get(j).copied().unwrap_or(' ');
match smush::smush_pair(l, r, rules, hardblank) {
Some(c) => merged.push(c),
None => {
break 'outer;
}
}
}
row_merges.push(merged);
}
overlap = k;
let _ = row_merges;
}
for (i, row) in rows.iter_mut().enumerate() {
let acc_chars: Vec<char> = row.chars().collect();
let glyph_row: Vec<char> = glyph_chars.get(i).cloned().unwrap_or_default();
let keep = acc_chars.len().saturating_sub(overlap);
let mut new_row: String = acc_chars[..keep].iter().collect();
for j in 0..overlap {
let l = acc_chars.get(keep + j).copied().unwrap_or(' ');
let r = glyph_row.get(j).copied().unwrap_or(' ');
let merged = smush::smush_pair(l, r, rules, hardblank).unwrap_or(r);
new_row.push(merged);
}
for j in overlap..glyph_width {
new_row.push(glyph_row.get(j).copied().unwrap_or(' '));
}
*row = new_row;
}
*visual_width = visual_width
.saturating_add(glyph_width)
.saturating_sub(overlap);
}
fn interleave_wrapped(all_rows: Vec<String>, height: usize) -> Vec<String> {
let has_wrap = all_rows.iter().any(|r| r.contains('\n'));
if !has_wrap {
return all_rows;
}
let per_row: Vec<Vec<&str>> = all_rows.iter().map(|r| r.split('\n').collect()).collect();
let segments = per_row.first().map(Vec::len).unwrap_or(0);
let mut out: Vec<String> = Vec::with_capacity(height * segments);
for seg in 0..segments {
for row_lines in per_row.iter().take(height) {
let s = row_lines.get(seg).copied().unwrap_or("");
out.push(s.to_owned());
}
}
out
}
fn apply_justify(
rows: Vec<String>,
justify: Justify,
width: u32,
print_direction: u32,
) -> Vec<String> {
let effective = match justify {
Justify::Center => Justify::Center,
Justify::Left => Justify::Left,
Justify::Right => Justify::Right,
Justify::FontDefault => {
if print_direction == 1 {
Justify::Right
} else {
Justify::Left
}
}
};
let target = width as usize;
rows.into_iter()
.map(|line| match effective {
Justify::Left | Justify::FontDefault => line,
Justify::Center => {
let w = line.chars().count();
if w >= target {
line
} else {
let pad = (target - w) / 2;
let mut out = String::with_capacity(target);
for _ in 0..pad {
out.push(' ');
}
out.push_str(&line);
out
}
}
Justify::Right => {
let w = line.chars().count();
if w >= target {
line
} else {
let pad = target - w;
let mut out = String::with_capacity(target);
for _ in 0..pad {
out.push(' ');
}
out.push_str(&line);
out
}
}
})
.collect()
}
fn strip_hardblanks(rows: Vec<String>, hardblank: char) -> Vec<String> {
rows.into_iter()
.map(|line| line.replace(hardblank, " "))
.collect()
}
pub fn clamp_input_latin1(input: &str) -> Vec<u8> {
let mut out = Vec::with_capacity(input.len());
for ch in input.chars() {
let cp = ch as u32;
if cp <= 0xFF {
out.push(cp as u8);
} else {
out.push(b'?');
}
}
out
}
static MISSING_GLYPH_WARNED: OnceLock<()> = OnceLock::new();
fn warn_missing_codepoint(cp: u32) {
if MISSING_GLYPH_WARNED.set(()).is_ok() {
eprintln!(
"rusty-figlet: codepoint U+{cp:04X} missing from font; substituting fallback glyph"
);
}
}
static OVER_WIDTH_WARNED: OnceLock<()> = OnceLock::new();
fn warn_over_width(word: &str, width: usize) {
if OVER_WIDTH_WARNED.set(()).is_ok() {
eprintln!(
"rusty-figlet: '{word}' too wide for width {width}; emitting at full glyph width"
);
}
}
#[derive(Debug, Clone)]
pub struct Banner {
rows: Vec<String>,
height: u32,
}
impl Banner {
pub fn lines(&self) -> impl Iterator<Item = String> + '_ {
self.rows.iter().cloned()
}
pub fn height(&self) -> u32 {
self.height
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty() || self.rows.iter().all(|r| r.is_empty())
}
}
impl core::fmt::Display for Banner {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
for line in self.lines() {
writeln!(f, "{line}")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use static_assertions::assert_impl_all;
assert_impl_all!(FigletBuilder: Send, Sync);
assert_impl_all!(Figlet: Send, Sync);
assert_impl_all!(Banner: Send, Sync);
assert_impl_all!(FigletError: Send, Sync);
fn _figlet_error_is_static() {
fn assert_static<T: 'static>() {}
assert_static::<FigletError>();
}
#[test]
fn builder_default_font_is_standard() {
let builder = FigletBuilder::new();
match builder.source {
FontSource::Bundled(Font::Standard) => {}
_ => panic!("default font must be Standard"),
}
}
}