1use cli_boilerplate_automation::bait::TransformExt;
2use ratatui::{
3 layout::Constraint,
4 style::Style,
5 text::{Line, Text},
6 widgets::{Cell, Paragraph, Row, Table},
7};
8
9use crate::{
10 config::{DisplayConfig, RowConnectionStyle},
11 utils::{serde::StringOrVec, text::wrap_text},
12};
13pub type HeaderTable = Vec<Vec<Line<'static>>>;
14#[derive(Debug)]
15pub struct DisplayUI {
16 width: u16,
17 height: u16,
18 text: Vec<Text<'static>>,
19 header: HeaderTable, pub show: bool,
21 pub config: DisplayConfig,
22}
23
24impl DisplayUI {
25 pub fn new(config: DisplayConfig) -> Self {
26 let (text, height) = match &config.content {
27 Some(StringOrVec::String(s)) => {
28 let text = Text::from(s.clone());
29 let height = text.height() as u16;
30 (vec![text], height)
31 }
32 Some(StringOrVec::Vec(s)) => {
33 let text: Vec<_> = s.iter().map(|s| Text::from(s.clone())).collect();
34 let height = text.iter().map(|t| t.height()).max().unwrap_or_default() as u16;
35 (text, height)
36 }
37 _ => (vec![], 0),
38 };
39
40 Self {
41 height,
42 width: 0,
43 show: config.content.is_some() || config.header_lines > 0,
44 header: Vec::new(),
45 text,
46 config,
47 }
48 }
49
50 pub fn update_width(&mut self, width: u16) {
51 let border_w = self.config.border.width();
52 let new_w = width.saturating_sub(border_w);
53 if new_w != self.width {
54 self.width = new_w;
55 if self.config.wrap && self.single() {
57 let text = wrap_text(self.text[0].clone(), self.width).0;
58 self.text[0] = text;
59 }
60 }
61 }
62
63 pub fn height(&self) -> u16 {
64 if !self.show {
65 return 0;
66 }
67 let mut height = self.height;
68 height += self.config.border.height();
69
70 height
71 }
72
73 pub fn set(&mut self, text: impl Into<Text<'static>>) {
75 let (text, _) = wrap_text(text.into(), self.config.wrap as u16 * self.width);
76
77 self.text = vec![text];
78
79 self.show = true;
80 }
81
82 pub fn clear(&mut self, keep_header: bool) {
83 if !keep_header {
84 self.header.clear();
85 self.show = false;
86 } else if self.header.is_empty() {
87 self.show = false;
88 }
89
90 self.text.clear();
91 }
92
93 pub fn single(&self) -> bool {
95 self.text.len() == 1
96 }
97
98 pub fn header_table(&mut self, table: HeaderTable) {
99 self.header = table
100 }
101
102 pub fn make_display(
104 &mut self,
105 result_indentation: u16,
106 mut widths: Vec<u16>,
107 col_spacing: u16,
108 ) -> Table<'_> {
109 if self.text.is_empty() || widths.is_empty() {
110 return Table::default();
111 }
112
113 let block = {
114 let b = self.config.border.as_block();
115 if self.config.match_indent {
116 let mut padding = self.config.border.padding;
117
118 padding.left = result_indentation.saturating_sub(self.config.border.left());
119 widths[0] -= result_indentation;
120 b.padding(padding)
121 } else {
122 b
123 }
124 };
125
126 let style = Style::default()
127 .fg(self.config.fg)
128 .add_modifier(self.config.modifier);
129
130 let (cells, height) = if self.single() {
131 let cells = if self.text.len() > 1 {
134 vec![]
135 } else {
136 vec![Cell::from(self.text[0].clone())]
137 };
138 let height = self.text[0].height() as u16;
139
140 (cells, height)
141 } else {
142 let mut height = 0;
143 let cells = self
145 .text
146 .iter()
147 .cloned()
148 .zip(widths.iter().copied())
149 .map(|(text, width)| {
150 let ret = wrap_text(text, width).0;
151 height = height.max(ret.height() as u16);
152
153 Cell::from(ret.transform_if(
154 matches!(
155 self.config.row_connection_style,
156 RowConnectionStyle::Disjoint
157 ),
158 |r| r.style(style),
159 ))
160 })
161 .collect();
162
163 (cells, height)
164 };
165
166 let row = Row::new(cells).style(style).height(height);
167 let mut rows = vec![row];
168 self.height = height;
169
170 if !self.header.is_empty() {
172 rows.extend(self.header.iter().cloned().map(Row::new));
174
175 self.height += height;
176 }
177
178 let widths = if self.single() {
179 vec![Constraint::Percentage(100)]
180 } else {
181 widths.into_iter().map(Constraint::Length).collect()
182 };
183
184 Table::new(rows, widths)
185 .block(block)
186 .column_spacing(col_spacing)
187 .transform_if(
188 !matches!(
189 self.config.row_connection_style,
190 RowConnectionStyle::Disjoint
191 ),
192 |t| t.style(style),
193 )
194 }
195
196 pub fn make_full_width_row(&self, result_indentation: u16) -> Paragraph<'_> {
198 let style = Style::default()
199 .fg(self.config.fg)
200 .add_modifier(self.config.modifier);
201
202 let left = if self.config.match_indent {
204 result_indentation.saturating_sub(self.config.border.left())
205 } else {
206 self.config.border.left()
207 };
208 let top = self.config.border.top();
209 let right = self.config.border.width().saturating_sub(left);
210 let bottom = self.config.border.height() - top;
211
212 let block = ratatui::widgets::Block::default().padding(ratatui::widgets::Padding {
213 left,
214 top,
215 right,
216 bottom,
217 });
218
219 Paragraph::new(self.text[0].clone())
220 .block(block)
221 .style(style)
222 }
223}