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