1use std::fmt::Display;
18
19use tabled::Table;
20use tabled::builder::Builder;
21use tabled::settings::Format;
22use tabled::settings::Modify;
23use tabled::settings::Panel;
24use tabled::settings::Remove;
25use tabled::settings::Width;
26use tabled::settings::object::{Columns, Rows, Segment};
27use tabled::settings::style::Style;
28use terminal_size::Width as TermWidth;
29use terminal_size::terminal_size;
30
31pub use tabled::settings::Padding;
32
33pub const DEFAULT_TERMINAL_WIDTH: usize = 120;
35
36#[must_use]
40pub fn terminal_width() -> usize {
41 terminal_size()
42 .map(|(TermWidth(w), _)| w as usize)
43 .unwrap_or(DEFAULT_TERMINAL_WIDTH)
44}
45
46#[must_use]
51pub fn responsive_width(utilization: f64) -> usize {
52 let width = terminal_width();
53 (width as f64 * utilization.clamp(0.0, 1.0)) as usize
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
58#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
59pub enum TableStyle {
60 #[default]
62 Modern,
63 Borderless,
65 Markdown,
67 Sharp,
69 Ascii,
71 Psql,
73 Dots,
75}
76
77#[cfg(feature = "clap")]
78impl clap::ValueEnum for TableStyle {
79 fn value_variants<'a>() -> &'a [Self] {
80 &[
81 Self::Modern,
82 Self::Borderless,
83 Self::Markdown,
84 Self::Sharp,
85 Self::Ascii,
86 Self::Psql,
87 Self::Dots,
88 ]
89 }
90
91 fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
92 Some(clap::builder::PossibleValue::new(match self {
93 Self::Modern => "modern",
94 Self::Borderless => "borderless",
95 Self::Markdown => "markdown",
96 Self::Sharp => "sharp",
97 Self::Ascii => "ascii",
98 Self::Psql => "psql",
99 Self::Dots => "dots",
100 }))
101 }
102}
103
104impl TableStyle {
105 pub fn apply(self, table: &mut Table) {
107 match self {
108 TableStyle::Modern => {
109 table.with(Style::rounded());
110 }
111 TableStyle::Borderless => {
112 table.with(Style::blank());
113 }
114 TableStyle::Markdown => {
115 table.with(Style::markdown());
116 }
117 TableStyle::Sharp => {
118 table.with(Style::sharp());
119 }
120 TableStyle::Ascii => {
121 table.with(Style::ascii());
122 }
123 TableStyle::Psql => {
124 table.with(Style::psql());
125 }
126 TableStyle::Dots => {
127 table.with(Style::dots());
128 }
129 }
130 }
131}
132
133pub struct StyledTable {
135 style: TableStyle,
136 header: Option<String>,
137 remove_header_row: bool,
138 padding: Option<Padding>,
139 newline_replacement: Option<String>,
140 max_width: Option<usize>,
141 wrap_column: Option<(usize, usize)>,
142}
143
144impl Default for StyledTable {
145 fn default() -> Self {
146 Self::new()
147 }
148}
149
150impl StyledTable {
151 #[must_use]
153 pub fn new() -> Self {
154 Self {
155 style: TableStyle::default(),
156 header: None,
157 remove_header_row: false,
158 padding: None,
159 newline_replacement: None,
160 max_width: None,
161 wrap_column: None,
162 }
163 }
164
165 #[must_use]
167 pub fn max_width(mut self, width: usize) -> Self {
168 self.max_width = Some(width);
169 self
170 }
171
172 #[must_use]
176 pub fn wrap_column(mut self, column_index: usize, width: usize) -> Self {
177 self.wrap_column = Some((column_index, width));
178 self
179 }
180
181 pub fn style(mut self, style: TableStyle) -> Self {
183 self.style = style;
184 self
185 }
186
187 pub fn header(mut self, header: impl Into<String>) -> Self {
189 self.header = Some(header.into());
190 self
191 }
192
193 pub fn remove_header_row(mut self) -> Self {
197 self.remove_header_row = true;
198 self
199 }
200
201 pub fn padding(mut self, padding: Padding) -> Self {
205 self.padding = Some(padding);
206 self
207 }
208
209 pub fn replace_newlines(mut self, replacement: impl Into<String>) -> Self {
214 self.newline_replacement = Some(replacement.into());
215 self
216 }
217
218 pub fn build<T: tabled::Tabled>(self, data: Vec<T>) -> Table {
220 let mut table = Table::new(data);
221
222 self.style.apply(&mut table);
223
224 if let Some(padding) = self.padding {
225 table.with(padding);
226 }
227
228 if self.remove_header_row {
230 table.with(Remove::row(Rows::first()));
231 }
232
233 if let Some(header) = self.header {
234 table.with(Panel::header(header));
235 }
236
237 if let Some(replacement) = self.newline_replacement {
238 table.with(
239 Modify::new(Segment::all())
240 .with(Format::content(move |s| s.replace('\n', &replacement))),
241 );
242 }
243
244 if let Some((col_idx, width)) = self.wrap_column {
245 table.with(Modify::new(Columns::new(col_idx..=col_idx)).with(Width::wrap(width)));
246 }
247
248 if let Some(width) = self.max_width {
249 table.with(Width::truncate(width));
250 }
251
252 table
253 }
254}
255
256#[must_use]
260pub fn display_option<T: Display>(opt: &Option<T>) -> String {
261 opt.as_ref().map_or_else(String::new, |val| val.to_string())
262}
263
264#[must_use]
266pub fn display_option_or<T: Display>(opt: &Option<T>, default: &str) -> String {
267 opt.as_ref()
268 .map_or_else(|| default.to_string(), |val| val.to_string())
269}
270
271#[must_use]
275pub fn parse_columns(columns_arg: &str) -> Vec<String> {
276 columns_arg
277 .split(',')
278 .map(|s| s.trim().to_lowercase())
279 .filter(|s| !s.is_empty())
280 .collect()
281}
282
283#[must_use]
287pub fn build_table_with_columns<T: tabled::Tabled>(data: &[T], columns: &[String]) -> Table {
288 let mut builder = Builder::default();
289
290 let headers: Vec<String> = T::headers()
291 .into_iter()
292 .map(|c| c.to_string().to_lowercase())
293 .collect();
294
295 let valid_columns: Vec<(usize, &String)> = columns
296 .iter()
297 .filter_map(|col| headers.iter().position(|h| h == col).map(|idx| (idx, col)))
298 .collect();
299
300 builder.push_record(valid_columns.iter().map(|(_, col)| col.as_str()));
301
302 for item in data {
303 let fields: Vec<String> = item.fields().into_iter().map(|c| c.to_string()).collect();
304
305 let row: Vec<&str> = valid_columns
306 .iter()
307 .map(|(idx, _)| fields[*idx].as_str())
308 .collect();
309 builder.push_record(row);
310 }
311
312 builder.build()
313}