1use anyhow::{anyhow, Result};
21use crossterm::{
22 execute,
23 style::{Color, Colors, Print, SetColors},
24};
25use std::{cell::RefCell, fmt, rc::Rc, usize::MAX};
26
27mod macros;
28pub use macros::*;
29
30pub type SafeGridHeader = Rc<RefCell<GridHeader>>;
31
32#[derive(Default, Clone, Eq, PartialEq, PartialOrd, Ord)]
35pub struct HeaderList(Vec<SafeGridHeader>);
36
37impl HeaderList {
38 pub fn new() -> Self {
39 Self::default()
40 }
41
42 pub fn is_empty(&self) -> bool {
43 self.0.len() == 0
44 }
45
46 pub fn len(&self) -> usize {
47 self.0.len()
48 }
49
50 pub fn get(&self, idx: usize) -> Option<&SafeGridHeader> {
51 self.0.get(idx)
52 }
53}
54
55impl fmt::Display for HeaderList {
56 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57 for header in self.0.clone() {
58 write!(
59 formatter,
60 "{:<width$}",
61 header.borrow().text,
62 width = header
63 .borrow()
64 .max_len
65 .unwrap_or(header.borrow().text.len() + 2)
66 )?
67 }
68 Ok(())
69 }
70}
71
72#[derive(Clone, PartialEq, Eq, Debug)]
77pub struct GridHeader {
78 index: Option<usize>,
79 text: &'static str,
80 min_size: Option<usize>,
81 max_pad: Option<usize>,
82 priority: usize,
83 max_len: Option<usize>,
84}
85
86impl Default for GridHeader {
87 fn default() -> Self {
88 Self {
89 index: None,
90 text: "",
91 min_size: None,
92 max_pad: Some(4),
93 priority: 0,
94 max_len: None,
95 }
96 }
97}
98
99impl PartialOrd for GridHeader {
100 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
101 if self.index.is_some() {
102 Some(
103 self.priority
104 .cmp(&other.priority)
105 .then(self.index.cmp(&other.index)),
106 )
107 } else {
108 Some(self.priority.cmp(&other.priority))
109 }
110 }
111}
112
113impl Ord for GridHeader {
114 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
115 if self.index.is_some() {
116 self.priority
117 .cmp(&other.priority)
118 .then(self.index.cmp(&other.index))
119 } else {
120 self.priority.cmp(&other.priority)
121 }
122 }
123}
124
125impl GridHeader {
126 pub fn set_max_len(&mut self, len: usize) {
128 self.max_len = Some(len)
129 }
130
131 pub fn set_text(mut self, text: &'static str) -> Self {
133 self.text = text;
134 self
135 }
136
137 pub fn set_priority(mut self, priority: usize) -> Self {
140 self.priority = priority;
141 self
142 }
143
144 pub fn set_index(&mut self, idx: usize) {
146 self.index = Some(idx);
147 }
148
149 pub fn text(&self) -> &str {
150 self.text.clone()
151 }
152
153 pub fn priority(&self) -> usize {
154 self.priority
155 }
156}
157
158#[derive(Clone, Debug, Default)]
161pub struct GridItem {
162 header: SafeGridHeader,
163 contents: String,
164 max_len: Option<usize>,
165}
166
167impl GridItem {
168 pub fn new(header: SafeGridHeader, contents: String) -> Self {
169 Self {
170 header,
171 contents,
172 max_len: None,
173 }
174 }
175
176 fn len(&self) -> usize {
177 self.contents.len() + 1 }
179
180 fn set_max_len(&mut self, max_len: usize) {
181 self.max_len = Some(max_len)
182 }
183}
184
185impl fmt::Display for GridItem {
186 fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
187 write!(
188 formatter,
189 "{:<max_len$}",
190 self.contents,
191 max_len = self.max_len.unwrap_or(self.len())
192 )
193 }
194}
195
196#[derive(Clone)]
200pub struct TTYGrid {
201 headers: HeaderList,
202 selected: HeaderList,
203 lines: Vec<GridLine>,
204 width: usize,
205 header_color: Colors,
206 delimiter_color: Colors,
207 primary_color: Colors,
208 secondary_color: Colors,
209}
210
211impl TTYGrid {
212 pub fn new(headers: Vec<SafeGridHeader>) -> Result<Self> {
213 let (w, _) = crossterm::terminal::size()?;
214 let width = w as usize;
215
216 Ok(Self {
217 selected: HeaderList::new(),
218 headers: HeaderList(headers),
219 lines: Vec::new(),
220 width,
221 header_color: Colors::new(Color::Reset, Color::Reset),
222 delimiter_color: Colors::new(Color::Reset, Color::Reset),
223 primary_color: Colors::new(Color::Reset, Color::Reset),
224 secondary_color: Colors::new(Color::Reset, Color::Reset),
225 })
226 }
227
228 pub fn set_delimiter_color(&mut self, colors: Colors) {
230 self.delimiter_color = colors
231 }
232
233 pub fn set_header_color(&mut self, colors: Colors) {
235 self.header_color = colors
236 }
237
238 pub fn set_primary_color(&mut self, colors: Colors) {
241 self.primary_color = colors
242 }
243
244 pub fn set_secondary_color(&mut self, colors: Colors) {
247 self.secondary_color = colors
248 }
249
250 pub fn add_line(&mut self, item: GridLine) {
251 self.lines.push(item)
252 }
253
254 pub fn clear_lines(&mut self) {
255 self.lines.clear()
256 }
257
258 pub fn headers(&self) -> HeaderList {
259 self.headers.clone()
260 }
261
262 pub fn select(&mut self, header: SafeGridHeader, idx: usize) {
263 header.borrow_mut().set_index(idx);
265 self.selected.0.push(header)
266 }
267
268 pub fn is_selected(&self, header: SafeGridHeader) -> bool {
269 self.selected.0.contains(&header)
270 }
271
272 pub fn select_all_headers(&mut self) {
273 self.selected = self.headers.clone()
274 }
275
276 pub fn deselect_all_headers(&mut self) {
277 self.selected.0.clear()
278 }
279
280 fn set_grid_max_len(&mut self, len_map: &LengthMapper) -> Result<()> {
281 let mut cached_columns = Vec::new();
282
283 for (idx, header) in self.headers.0.iter_mut().enumerate() {
284 let max_len = len_map.max_len_for_column(&header.borrow())?;
285 header.borrow_mut().set_max_len(max_len);
286 cached_columns.insert(idx, header.borrow().max_len);
287 }
288
289 for line in self.lines.iter_mut() {
290 for (idx, item) in line.0.iter_mut().enumerate() {
291 if let Some(column) = cached_columns.get(idx) {
292 item.set_max_len(column.unwrap());
293 }
294 }
295 }
296
297 Ok(())
298 }
299
300 fn determine_headers(&mut self) -> Result<()> {
301 let mut len_map = LengthMapper::default();
302 len_map.map_lines(self.lines.clone());
303
304 self.set_grid_max_len(&len_map)?; let last = len_map.max_len_for_headers(self.headers.clone())?;
306
307 if last <= self.width {
308 self.select_all_headers();
309 return Ok(());
310 }
311
312 let mut prio_map: Vec<(usize, (HeaderList, usize))> = Vec::new();
313 self.deselect_all_headers();
314
315 let mut len = self.headers.0.len();
316
317 while len > 0 {
318 let mut headers = HeaderList::new();
319 for header in self.headers.0.iter().take(len) {
320 headers.0.push(header.clone())
321 }
322
323 let mut max_len = len_map.max_len_for_headers(headers.clone())?;
324
325 while max_len > self.width {
326 let mut new_headers = headers.clone();
327 let mut lowest_prio_index = MAX;
328 let mut to_remove = None;
329
330 for (idx, header) in new_headers.0.iter().enumerate() {
331 let priority = header.borrow().priority;
332 if priority < lowest_prio_index {
333 to_remove = Some(idx);
334 lowest_prio_index = priority;
335 }
336 }
337
338 if let Some(to_remove) = to_remove {
339 new_headers.0.remove(to_remove);
340 max_len = len_map.max_len_for_headers(new_headers.clone())?;
341 headers = new_headers;
342 } else {
343 max_len = 0 }
345 }
346
347 let index = headers.0.iter().fold(0, |acc, x| acc + x.borrow().priority);
348 prio_map.push((index, (headers, max_len)));
349 len -= 1;
350 }
351
352 if prio_map.is_empty() {
353 return Err(anyhow!("your terminal is too small"));
354 }
355
356 prio_map.sort();
357
358 let (_, (max_headers, _)) = prio_map.iter().last().unwrap();
359
360 for (idx, header) in max_headers.0.iter().enumerate() {
361 self.select(header.clone(), idx);
362 }
363
364 Ok(())
365 }
366
367 pub fn display(&mut self) -> Result<String> {
371 self.determine_headers()?;
372 Ok(format!("{}", self))
373 }
374
375 pub fn write(&mut self, mut writer: impl std::io::Write) -> Result<()> {
377 self.determine_headers()?;
378 execute!(
379 writer,
380 SetColors(self.header_color),
381 Print(&format!("{}\n", self.selected))
382 )?;
383 execute!(
384 writer,
385 SetColors(self.delimiter_color),
386 Print(&format!("{:-<width$}\n", "-", width = self.width))
387 )?;
388
389 for (idx, line) in self.lines.iter().enumerate() {
390 if idx % 2 == 0 {
391 execute!(writer, SetColors(self.primary_color))?;
392 } else {
393 execute!(writer, SetColors(self.secondary_color))?;
394 }
395 execute!(writer, Print(&format!("{}\n", line.selected(self))))?;
396 }
397
398 Ok(())
399 }
400}
401
402impl fmt::Display for TTYGrid {
403 fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
404 writeln!(formatter, "{}", self.selected)?;
405 writeln!(formatter, "{:-<width$}", "-", width = self.width)?;
406
407 for line in self.lines.clone() {
408 writeln!(formatter, "{}", line.selected(self))?
409 }
410
411 Ok(())
412 }
413}
414
415#[derive(Clone, Default, Debug)]
417pub struct GridLine(pub Vec<GridItem>);
418
419impl GridLine {
420 fn selected(&self, grid: &TTYGrid) -> Self {
421 let mut ret = Vec::new();
422 for item in self.0.iter() {
423 if grid.is_selected(item.header.clone()) {
424 ret.push(item.clone())
425 }
426 }
427
428 GridLine(ret)
429 }
430}
431
432impl fmt::Display for GridLine {
433 fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
434 for contents in self.0.clone() {
435 write!(formatter, "{}", contents)?
436 }
437
438 Ok(())
439 }
440}
441
442#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
443struct LengthMapper(Vec<Vec<(SafeGridHeader, usize)>>);
444
445impl LengthMapper {
446 fn map_lines(&mut self, lines: Vec<GridLine>) {
447 for line in lines.clone() {
448 let len = self.0.len();
449 self.0.push(Vec::new()); for item in line.0 {
451 self.0
452 .get_mut(len)
453 .unwrap()
454 .push((item.header.clone(), item.len()));
455 }
456 }
457 }
458
459 fn max_len_for_column(&self, header: &GridHeader) -> Result<usize> {
460 let mut max_len = 0;
461 for line in self.0.clone() {
462 let found = line.iter().find(|i| i.0.borrow().eq(header));
463
464 if found.is_none() {
465 return Err(anyhow!(
466 "panic: cannot find pre-existing column in line, report this bug"
467 ));
468 }
469
470 if max_len < found.unwrap().1 {
471 max_len = found.unwrap().1
472 }
473 }
474
475 Ok(max_len + header.max_pad.unwrap_or(0) + 2)
476 }
477
478 fn max_len_for_headers(&mut self, headers: HeaderList) -> Result<usize> {
479 Ok(headers.0.iter().fold(0, |x, h| {
480 x + self
481 .max_len_for_column(&h.clone().borrow())
482 .unwrap_or_default()
483 }))
484 }
485}