1use comfy_table::{Attribute, Cell, CellAlignment, Color, ContentArrangement, Table, presets};
7use owo_colors::OwoColorize;
8
9pub struct OutputStyle {
11 pub use_color: bool,
13 pub use_unicode: bool,
15}
16
17impl Default for OutputStyle {
18 fn default() -> Self {
19 Self {
20 use_color: std::env::var("NO_COLOR").is_err(),
22 use_unicode: true,
23 }
24 }
25}
26
27impl OutputStyle {
28 #[must_use]
30 pub fn new() -> Self {
31 Self::default()
32 }
33
34 #[must_use]
36 pub fn no_color(mut self) -> Self {
37 self.use_color = false;
38 self
39 }
40
41 #[must_use]
43 pub fn ascii(mut self) -> Self {
44 self.use_unicode = false;
45 self
46 }
47}
48
49pub fn format_header(text: &str, style: &OutputStyle) -> String {
51 if style.use_color {
52 text.bold().bright_blue().to_string()
53 } else {
54 text.to_string()
55 }
56}
57
58pub fn format_success(text: &str, style: &OutputStyle) -> String {
60 if style.use_color {
61 text.green().to_string()
62 } else {
63 text.to_string()
64 }
65}
66
67pub fn format_warning(text: &str, style: &OutputStyle) -> String {
69 if style.use_color {
70 text.yellow().to_string()
71 } else {
72 text.to_string()
73 }
74}
75
76pub fn format_error(text: &str, style: &OutputStyle) -> String {
78 if style.use_color {
79 text.red().to_string()
80 } else {
81 text.to_string()
82 }
83}
84
85pub fn format_key_value(key: &str, value: &str, style: &OutputStyle) -> String {
87 if style.use_color {
88 format!("{}: {}", key.cyan(), value)
89 } else {
90 format!("{key}: {value}")
91 }
92}
93
94pub fn create_table(style: &OutputStyle) -> Table {
96 let mut table = Table::new();
97
98 if style.use_unicode {
100 table
101 .load_preset(presets::UTF8_FULL)
102 .apply_modifier(comfy_table::modifiers::UTF8_ROUND_CORNERS);
103 } else {
104 table.load_preset(presets::ASCII_FULL);
105 }
106
107 table
109 .set_content_arrangement(ContentArrangement::Dynamic)
110 .set_width(140);
111
112 table
113}
114
115pub fn create_list_table(style: &OutputStyle) -> Table {
117 let mut table = Table::new();
118
119 if style.use_unicode {
120 table.load_preset(presets::UTF8_HORIZONTAL_ONLY);
121 } else {
122 table.load_preset(presets::ASCII_HORIZONTAL_ONLY);
123 }
124
125 table
126 .set_content_arrangement(ContentArrangement::Dynamic)
127 .set_width(100);
128
129 table
130}
131
132pub fn header_cell(text: &str, style: &OutputStyle) -> Cell {
134 let cell = Cell::new(text);
135 if style.use_color {
136 cell.fg(Color::Cyan)
137 .add_attribute(Attribute::Bold)
138 .set_alignment(CellAlignment::Left)
139 } else {
140 cell.add_attribute(Attribute::Bold)
141 .set_alignment(CellAlignment::Left)
142 }
143}
144
145pub fn regular_cell(text: &str) -> Cell {
147 Cell::new(text).set_alignment(CellAlignment::Left)
148}
149
150pub fn numeric_cell(text: &str) -> Cell {
152 Cell::new(text).set_alignment(CellAlignment::Right)
153}
154
155pub fn status_cell(text: &str, style: &OutputStyle) -> Cell {
157 let cell = Cell::new(text);
158 if style.use_color {
159 match text {
160 "✓" | "OK" | "Success" | "Complete" => cell.fg(Color::Green),
161 "✗" | "Failed" | "Error" => cell.fg(Color::Red),
162 "⚠" | "Warning" => cell.fg(Color::Yellow),
163 "…" | "In Progress" => cell.fg(Color::Blue),
164 _ => cell,
165 }
166 } else {
167 cell
168 }
169}
170
171pub fn hash_cell(text: &str, style: &OutputStyle) -> Cell {
173 let cell = Cell::new(text);
174 if style.use_color {
175 cell.fg(Color::Grey)
176 } else {
177 cell
178 }
179}
180
181pub fn print_section_header(title: &str, style: &OutputStyle) {
183 if style.use_color {
184 println!("\n{}", title.bold().bright_blue());
185 println!("{}", "═".repeat(title.len()).bright_blue());
186 } else {
187 println!("\n{title}");
188 println!("{}", "=".repeat(title.len()));
189 }
190}
191
192pub fn print_subsection_header(title: &str, style: &OutputStyle) {
194 if style.use_color {
195 println!("\n{}", title.cyan());
196 println!("{}", "─".repeat(title.len()).cyan());
197 } else {
198 println!("\n{title}");
199 println!("{}", "-".repeat(title.len()));
200 }
201}
202
203pub fn format_count_badge(count: usize, item_name: &str, style: &OutputStyle) -> String {
205 let text = if count == 1 {
206 format!("({count} {item_name})")
207 } else {
208 format!("({count} {item_name}s)")
209 };
210
211 if style.use_color {
212 text.dimmed().to_string()
213 } else {
214 text
215 }
216}
217
218pub fn format_timestamp(timestamp: &str, style: &OutputStyle) -> String {
220 if style.use_color {
221 timestamp.dimmed().to_string()
222 } else {
223 timestamp.to_string()
224 }
225}
226
227pub fn format_path(path: &str, style: &OutputStyle) -> String {
229 if style.use_color {
230 path.bright_magenta().to_string()
231 } else {
232 path.to_string()
233 }
234}
235
236pub fn format_url(url: &str, style: &OutputStyle) -> String {
238 if style.use_color {
239 url.bright_blue().underline().to_string()
240 } else {
241 url.to_string()
242 }
243}
244
245pub fn format_hash(hash: &str, style: &OutputStyle) -> String {
247 if style.use_color {
248 hash.dimmed().italic().to_string()
249 } else {
250 hash.to_string()
251 }
252}