1#[allow(unused)]
2use log::debug;
3
4use ratatui::{
5 layout::{Alignment, Rect},
6 style::{Style, Stylize},
7 widgets::{Paragraph, Row, Table},
8};
9use unicode_width::UnicodeWidthStr;
10
11use crate::{
12 SSS, Selection, Selector,
13 config::{ResultsConfig, RowConnectionStyle},
14 nucleo::{Status, Worker},
15 render::Click,
16 utils::{
17 seperator::HorizontalSeparator,
18 text::{clip_text_lines, fit_width, prefix_text, substitute_escaped},
19 },
20};
21
22#[derive(Debug)]
23pub struct ResultsUI {
24 cursor: u16,
25 bottom: u16,
26 height: u16, width: u16,
28 widths: Vec<u16>,
31 col: Option<usize>,
32 pub status: Status,
33 pub config: ResultsConfig,
34
35 pub cursor_disabled: bool,
36}
37
38impl ResultsUI {
39 pub fn new(config: ResultsConfig) -> Self {
40 Self {
41 cursor: 0,
42 bottom: 0,
43 col: None,
44 widths: Vec::new(),
45 status: Default::default(),
46 height: 0, width: 0,
48 config,
49 cursor_disabled: false,
50 }
51 }
52 pub fn update_dimensions(&mut self, area: &Rect) {
54 let [bw, bh] = [self.config.border.height(), self.config.border.width()];
55 self.width = area.width.saturating_sub(bw);
56 self.height = area.height.saturating_sub(bh);
57 }
58
59 pub fn table_width(&self) -> u16 {
60 self.config.column_spacing.0 * self.widths().len().saturating_sub(1) as u16
61 + self.widths.iter().sum::<u16>()
62 + self.config.border.width()
63 }
64
65 pub fn reverse(&self) -> bool {
67 self.config.reverse.is_always()
68 }
69 pub fn is_wrap(&self) -> bool {
70 self.config.wrap
71 }
72 pub fn wrap(&mut self, wrap: bool) {
73 self.config.wrap = wrap;
74 }
75
76 pub fn toggle_col(&mut self, col_idx: usize) -> bool {
79 if self.col == Some(col_idx) {
80 self.col = None
81 } else {
82 self.col = Some(col_idx);
83 }
84 self.col.is_some()
85 }
86 pub fn cycle_col(&mut self) {
87 self.col = match self.col {
88 None => self.widths.is_empty().then_some(0),
89 Some(c) => {
90 let next = c + 1;
91 if next < self.widths.len() {
92 Some(next)
93 } else {
94 None
95 }
96 }
97 };
98 }
99
100 fn scroll_padding(&self) -> u16 {
102 self.config.scroll_padding.min(self.height / 2)
103 }
104 pub fn end(&self) -> u32 {
105 self.status.matched_count.saturating_sub(1)
106 }
107
108 pub fn index(&self) -> u32 {
112 if self.cursor_disabled {
113 u32::MAX
114 } else {
115 (self.cursor + self.bottom) as u32
116 }
117 }
118 pub fn cursor_prev(&mut self) {
126 if self.cursor <= self.scroll_padding() && self.bottom > 0 {
127 self.bottom -= 1;
128 } else if self.cursor > 0 {
129 self.cursor -= 1;
130 } else if self.config.scroll_wrap {
131 self.cursor_jump(self.end());
132 }
133 }
134 pub fn cursor_next(&mut self) {
135 if self.cursor_disabled {
136 self.cursor_disabled = false
137 }
138
139 log::trace!(
140 "Cursor {} @ index {}. Status: {:?}.",
141 self.cursor,
142 self.index(),
143 self.status
144 );
145
146 if self.cursor + 1 + self.scroll_padding() >= self.height
147 && self.bottom + self.height < self.status.matched_count as u16
148 {
149 self.bottom += 1;
150 } else if self.index() < self.end() {
151 self.cursor += 1;
152 } else if self.config.scroll_wrap {
153 self.cursor_jump(0)
154 }
155 }
156
157 pub fn cursor_jump(&mut self, index: u32) {
158 self.cursor_disabled = false;
159
160 let end = self.end();
161 let index = index.min(end) as u16;
162
163 if index < self.bottom || index >= self.bottom + self.height {
164 self.bottom = (end as u16 + 1).saturating_sub(self.height).min(index);
165 self.cursor = index - self.bottom;
166 } else {
167 self.cursor = index - self.bottom;
168 }
169 }
170
171 pub fn indentation(&self) -> usize {
173 self.config.multi_prefix.width()
174 }
175 pub fn col(&self) -> Option<usize> {
176 self.col
177 }
178
179 pub fn widths(&self) -> &Vec<u16> {
182 &self.widths
183 }
184 pub fn width(&self) -> u16 {
186 self.width.saturating_sub(self.indentation() as u16)
187 }
188
189 pub fn max_widths(&self) -> Vec<u16> {
191 if !self.config.wrap {
192 return vec![];
193 }
194
195 let mut widths = vec![u16::MAX; self.widths.len()];
196
197 let total: u16 = self.widths.iter().sum();
198 if total <= self.width() {
199 return vec![];
200 }
201
202 let mut available = self.width();
203 let mut scale_total = 0;
204 let mut scalable_indices = Vec::new();
205
206 for (i, &w) in self.widths.iter().enumerate() {
207 if w <= self.config.wrap_scaling_min_width {
208 available = available.saturating_sub(w);
209 } else {
210 scale_total += w;
211 scalable_indices.push(i);
212 }
213 }
214
215 for &i in &scalable_indices {
216 let old = self.widths[i];
217 let new_w = old * available / scale_total;
218 widths[i] = new_w.max(self.config.wrap_scaling_min_width);
219 }
220
221 if let Some(&last_idx) = scalable_indices.last() {
223 let used_total: u16 = widths.iter().sum();
224 if used_total < self.width() {
225 widths[last_idx] += self.width() - used_total;
226 }
227 }
228
229 widths
230 }
231
232 pub fn make_table<'a, T: SSS>(
235 &mut self,
236 worker: &'a mut Worker<T>,
237 selector: &mut Selector<T, impl Selection>,
238 matcher: &mut nucleo::Matcher,
239 click: &mut Click,
240 ) -> Table<'a> {
241 let offset = self.bottom as u32;
242 let end = (self.bottom + self.height) as u32;
243 let hz = !self.config.stacked_columns;
244
245 let width_limits = if hz {
246 self.max_widths()
247 } else {
248 vec![
249 if self.config.wrap {
250 self.width
251 } else {
252 u16::MAX
253 };
254 worker.columns.len()
255 ]
256 };
257
258 let (mut results, mut widths, status) =
259 worker.results(offset, end, &width_limits, self.match_style(), matcher);
260
261 let match_count = status.matched_count;
262 self.status = status;
263
264 if match_count < (self.bottom + self.cursor) as u32 && !self.cursor_disabled {
265 self.cursor_jump(match_count);
266 } else {
267 self.cursor = self.cursor.min(results.len().saturating_sub(1) as u16)
268 }
269
270 widths[0] += self.indentation() as u16;
271
272 let mut rows = vec![];
273 let mut total_height = 0;
274
275 if results.is_empty() {
276 return Table::new(rows, widths);
277 }
278
279 let height_of = |t: &(Vec<ratatui::text::Text<'a>>, _, u16)| {
280 self._hr()
281 + if hz {
282 t.2
283 } else {
284 t.0.iter().map(|t| t.height() as u16).sum::<u16>()
285 }
286 };
287
288 let cursor_result_h = results[self.cursor as usize].2;
290 let mut start_index = 0;
292
293 let cum_h_after_cursor = results[(self.cursor as usize + 1).min(results.len())..]
294 .iter()
295 .map(height_of)
296 .sum();
297
298 let cursor_should_lt = self.height - self.scroll_padding().min(cum_h_after_cursor);
299 let mut partial = false;
300
301 if cursor_result_h >= cursor_should_lt {
302 start_index = self.cursor;
303 self.bottom += self.cursor;
304 self.cursor = 0;
305 } else
306 if let cum_h_to_cursor_end = results[0..=self.cursor as usize]
308 .iter()
309 .map(height_of)
310 .sum::<u16>()
311 && cum_h_to_cursor_end > cursor_should_lt
312 {
313 start_index = 1;
314 let mut remaining_height = cum_h_to_cursor_end - cursor_should_lt;
315 for r in results[..self.cursor as usize].iter_mut() {
318 let h = height_of(r);
319 let (row, item, _) = r;
320
321 if remaining_height < h {
322 let prefix = if selector.contains(item) {
323 self.config.multi_prefix.clone().to_string()
324 } else {
325 self.default_prefix(0)
326 };
327
328 total_height += remaining_height;
329
330 if hz {
331 for (_, t) in row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0) {
332 clip_text_lines(t, remaining_height, !self.reverse());
333 }
334
335 prefix_text(&mut row[0], prefix);
336
337 let last_visible = widths
338 .iter()
339 .enumerate()
340 .rev()
341 .find_map(|(i, w)| (*w != 0).then_some(i));
342
343 let mut row_texts: Vec<_> = row
344 .iter()
345 .take(last_visible.map(|x| x + 1).unwrap_or(0))
346 .cloned()
347 .collect();
348
349 if self.config.right_align_last && row_texts.len() > 1 {
350 row_texts.last_mut().unwrap().alignment = Some(Alignment::Right)
351 }
352
353 let row = Row::new(row_texts).height(remaining_height);
354 rows.push(row);
355 } else {
356 let mut push = vec![];
357
358 for col in row.into_iter().rev() {
359 let mut height = col.height() as u16;
360 if remaining_height == 0 {
361 break;
362 } else if remaining_height < height {
363 clip_text_lines(col, remaining_height, !self.reverse());
364 height = remaining_height;
365 }
366 remaining_height -= height;
367 prefix_text(col, prefix.clone());
368 push.push(Row::new(vec![col.clone()]).height(height));
369 }
370 rows.extend(push);
371 }
372
373 self.bottom += start_index - 1;
374 self.cursor -= start_index - 1;
375 partial = true;
376 break;
377 } else if remaining_height == h {
378 self.bottom += start_index;
379 self.cursor -= start_index;
380 break;
382 }
383
384 start_index += 1;
385 remaining_height -= h;
386 }
387 }
388
389 let mut remaining_height = self.height.saturating_sub(total_height);
395
396 for (mut i, (mut row, item, mut height)) in
397 results.drain(start_index as usize..).enumerate()
398 {
399 i += partial as usize;
400
401 if let Click::ResultPos(c) = click
403 && total_height > *c
404 {
405 let idx = self.bottom as u32 + i as u32 - 1;
406 log::debug!("Mapped click position to index: {c} -> {idx}",);
407 *click = Click::ResultIdx(idx);
408 }
409
410 if let Some(hr) = self.hr()
412 && remaining_height > 0
413 {
414 rows.push(hr);
415 remaining_height -= 1;
416 }
417 if remaining_height == 0 {
418 break;
419 }
420
421 let prefix = if selector.contains(item) {
423 self.config.multi_prefix.clone().to_string()
424 } else {
425 self.default_prefix(i)
426 };
427
428 if hz {
429 if remaining_height < height {
430 height = remaining_height;
431
432 for (_, t) in row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0) {
433 clip_text_lines(t, height, self.reverse());
434 }
435 }
436 remaining_height -= height;
437
438 prefix_text(&mut row[0], prefix);
439
440 let last_visible = widths
442 .iter()
443 .enumerate()
444 .rev()
445 .find_map(|(i, w)| (*w != 0).then_some(i));
446
447 let mut row_texts: Vec<_> = row
448 .iter()
449 .take(last_visible.map(|x| x + 1).unwrap_or(0))
450 .cloned()
451 .enumerate()
453 .map(|(x, t)| {
454 if self.is_current(i)
455 && (self.col.is_none()
456 && matches!(
457 self.config.row_connection_style,
458 RowConnectionStyle::Disjoint
459 )
460 || self.col == Some(x))
461 {
462 t.style(self.current_style())
463 } else {
464 t
465 }
466 })
467 .collect();
468
469 if self.config.right_align_last && row_texts.len() > 1 {
470 row_texts.last_mut().unwrap().alignment = Some(Alignment::Right)
471 }
472
473 let mut row = Row::new(row_texts).height(height);
475
476 if self.is_current(i)
477 && self.col.is_none()
478 && !matches!(
479 self.config.row_connection_style,
480 RowConnectionStyle::Disjoint
481 )
482 {
483 row = row.style(self.current_style())
484 }
485
486 rows.push(row);
487 } else {
488 let mut push = vec![];
489
490 for (x, mut col) in row.into_iter().enumerate() {
491 let mut height = col.height() as u16;
492
493 if remaining_height == 0 {
494 break;
495 } else if remaining_height < height {
496 height = remaining_height;
497 clip_text_lines(&mut col, remaining_height, !self.reverse());
498 }
499 remaining_height -= height;
500
501 prefix_text(&mut col, prefix.clone());
502
503 let mut row = Row::new(vec![col.clone()]).height(height);
505
506 if self.is_current(i) && (self.col.is_none() || self.col == Some(x)) {
507 row = row.style(self.current_style())
508 }
509
510 push.push(row);
511 }
512 log::debug!("{push:?}");
513 rows.extend(push);
514 }
515 }
516
517 if self.reverse() {
518 rows.reverse();
519 if total_height < self.height {
520 let spacer_height = self.height - total_height;
521 rows.insert(0, Row::new(vec![vec![]]).height(spacer_height));
522 }
523 }
524
525 if hz {
528 self.widths = {
529 let pos = widths.iter().rposition(|&x| x != 0).map_or(0, |p| p + 1);
530 let mut widths = widths[..pos].to_vec();
531 if pos > 2 && self.config.right_align_last {
532 let used = widths.iter().take(widths.len() - 1).sum();
533 widths[pos - 1] = self.width().saturating_sub(used);
534 }
535 widths
536 };
537 }
538
539 let mut table = Table::new(
541 rows,
542 if hz {
543 self.widths.clone()
544 } else {
545 vec![self.width]
546 },
547 )
548 .column_spacing(self.config.column_spacing.0)
549 .style(self.config.fg)
550 .add_modifier(self.config.modifier);
551
552 log::debug!("{table:?}");
553
554 table = table.block(self.config.border.as_static_block());
555 table
556 }
557
558 pub fn make_status(&self) -> Paragraph<'_> {
559 Paragraph::new(format!(
560 "{}{}/{}",
561 " ".repeat(self.indentation()),
562 &self.status.matched_count,
563 &self.status.item_count
564 ))
565 .style(self.config.status_fg)
566 .add_modifier(self.config.status_modifier)
567 }
568}
569
570impl ResultsUI {
572 fn default_prefix(&self, i: usize) -> String {
573 let substituted = substitute_escaped(
574 &self.config.default_prefix,
575 &[
576 ('d', &(i + 1).to_string()), ('r', &(i + 1 + self.bottom as usize).to_string()), ],
579 );
580
581 fit_width(&substituted, self.indentation())
582 }
583
584 fn current_style(&self) -> Style {
585 Style::from(self.config.current_fg)
586 .bg(self.config.current_bg)
587 .add_modifier(self.config.current_modifier)
588 }
589
590 fn is_current(&self, i: usize) -> bool {
591 !self.cursor_disabled && self.cursor == i as u16
592 }
593
594 pub fn match_style(&self) -> Style {
595 Style::default()
596 .fg(self.config.match_fg)
597 .add_modifier(self.config.match_modifier)
598 }
599
600 pub fn hr(&self) -> Option<Row<'static>> {
601 let sep = self.config.horizontal_separator;
602
603 if matches!(sep, HorizontalSeparator::None) {
604 return None;
605 }
606
607 if !self.config.stacked_columns && self.widths.len() > 1 {
609 return Some(Row::new(vec![vec![]]));
610 }
611
612 let unit = sep.as_str();
613 let line = unit.repeat(self.width as usize);
614
615 Some(Row::new(vec![line]))
616 }
617
618 pub fn _hr(&self) -> u16 {
619 self.hr().is_some() as u16
620 }
621}