1pub mod figlet;
23
24#[cfg(feature = "image")]
25pub mod ascii;
26
27#[cfg(feature = "image")]
28pub mod block;
29
30#[cfg(feature = "image")]
31pub mod imagediff;
32
33#[cfg(feature = "sixel")]
34pub mod sixel;
35
36#[cfg(feature = "image")]
39pub use image;
40
41#[cfg(feature = "gif")]
42pub mod gif;
43
44#[cfg(feature = "gif")]
45pub mod stage;
46
47pub use crate::figlet::{FigletFont, FontError, Justify};
48
49#[cfg(feature = "image")]
50pub use crate::ascii::{AsciiArt, DEFAULT_RAMP};
51
52#[cfg(feature = "image")]
53pub use crate::block::BlockArt;
54
55#[cfg(feature = "sixel")]
56pub use crate::sixel::SixelArt;
57
58#[cfg(feature = "image")]
59pub use crate::imagediff::{diff, DiffError, DiffReport, DiffSettings, Region};
60
61#[cfg(feature = "gif")]
62pub use crate::gif::{AnimatedArt, Repeat};
63
64#[cfg(feature = "gif")]
65pub use crate::stage::{Stage, Until};
66
67use rich::console::{Console, ConsoleOptions};
68use rich::protocol::Renderable;
69use rich::segment::Segment;
70use rich::style::Style;
71
72pub struct Figlet {
77 text: String,
78 font: FigletFont,
79 justify: Justify,
80 style: Option<Style>,
81 width: Option<usize>,
83}
84
85impl Figlet {
86 pub fn new(text: impl Into<String>) -> Self {
88 Figlet {
89 text: text.into(),
90 font: FigletFont::standard(),
91 justify: Justify::Left,
92 style: None,
93 width: None,
94 }
95 }
96
97 pub fn font(mut self, font: FigletFont) -> Self {
99 self.font = font;
100 self
101 }
102
103 pub fn justify(mut self, justify: Justify) -> Self {
105 self.justify = justify;
106 self
107 }
108
109 pub fn style(mut self, style: Style) -> Self {
111 self.style = Some(style);
112 self
113 }
114
115 pub fn width(mut self, width: usize) -> Self {
117 self.width = Some(width);
118 self
119 }
120
121 pub fn to_text(&self, width: usize) -> String {
124 figlet::render(&self.text, &self.font, width, self.justify)
125 }
126}
127
128impl Renderable for Figlet {
129 fn rich_render(&self, _console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
130 let width = self.width.unwrap_or(options.max_width);
131 let banner = self.to_text(width);
132 let lines: Vec<&str> = banner
135 .strip_suffix('\n')
136 .unwrap_or(&banner)
137 .split('\n')
138 .collect();
139 let mut segments = Vec::new();
140 let last = lines.len().saturating_sub(1);
141 for (index, line) in lines.into_iter().enumerate() {
142 segments.push(Segment::new(line.to_string(), self.style.clone()));
143 if index != last {
144 segments.push(Segment::line());
145 }
146 }
147 segments
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use rich::color::ColorSystem;
155
156 #[test]
157 fn renders_a_banner_through_a_console() {
158 let console = Console::builder()
159 .force_terminal(true)
160 .color_system(Some(ColorSystem::Truecolor))
161 .width(80)
162 .no_color(false)
163 .build();
164 let out = console.render_to_string(&Figlet::new("Hi").width(80));
165 assert!(out.contains("| | | (_)"), "got:\n{out}");
167 assert!(out.contains("|_| |_|_|"), "got:\n{out}");
168 }
169
170 #[test]
171 fn style_is_applied_to_every_row() {
172 let console = Console::builder()
173 .force_terminal(true)
174 .color_system(Some(ColorSystem::Truecolor))
175 .width(80)
176 .no_color(false)
177 .build();
178 let banner = Figlet::new("Hi")
179 .width(80)
180 .style(Style::parse("bold red").unwrap());
181 let out = console.render_to_string(&banner);
182 assert!(out.contains("\x1b[1;31m"), "expected bold red, got:\n{out}");
183 }
184
185 #[test]
186 fn to_text_matches_the_renderable() {
187 let banner = Figlet::new("Hi");
188 assert!(banner.to_text(80).starts_with(" _ _ _ \n"));
189 }
190}