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,
14 nucleo::{Status, Worker},
15 utils::text::{clip_text_lines, fit_width, prefix_text, substitute_escaped},
16};
17
18#[derive(Debug, Clone)]
20pub struct ResultsUI {
21 cursor: u16,
22 bottom: u16,
23 height: u16, width: u16,
25 widths: Vec<u16>, col: Option<usize>,
27 pub status: Status,
28 pub config: ResultsConfig,
29
30 pub cursor_disabled: bool,
31}
32
33impl ResultsUI {
34 pub fn new(config: ResultsConfig) -> Self {
35 Self {
36 cursor: 0,
37 bottom: 0,
38 col: None,
39 widths: Vec::new(),
40 status: Default::default(),
41 height: 0, width: 0,
43 config,
44 cursor_disabled: false,
45 }
46 }
47 pub fn update_dimensions(&mut self, area: &Rect) {
49 let [bw, bh] = [self.config.border.height(), self.config.border.width()];
50 self.width = area.width.saturating_sub(bw);
51 self.height = area.height.saturating_sub(bh);
52 }
53
54 pub fn reverse(&self) -> bool {
56 self.config.reverse.unwrap()
57 }
58 pub fn is_wrap(&self) -> bool {
59 self.config.wrap
60 }
61 pub fn wrap(&mut self, wrap: bool) {
62 self.config.wrap = wrap;
63 }
64
65 pub fn toggle_col(&mut self, col_idx: usize) -> bool {
68 if self.col == Some(col_idx) {
69 self.col = None
70 } else {
71 self.col = Some(col_idx);
72 }
73 self.col.is_some()
74 }
75 pub fn cycle_col(&mut self) {
76 self.col = match self.col {
77 None => {
78 if !self.widths.is_empty() {
79 Some(0)
80 } else {
81 None
82 }
83 }
84 Some(c) => {
85 let next = c + 1;
86 if next < self.widths.len() {
87 Some(next)
88 } else {
89 None
90 }
91 }
92 };
93 }
94
95 fn scroll_padding(&self) -> u16 {
97 self.config.scroll_padding.min(self.height / 2)
98 }
99 pub fn end(&self) -> u32 {
100 self.status.matched_count.saturating_sub(1)
101 }
102 pub fn index(&self) -> u32 {
103 if self.cursor_disabled {
104 u32::MAX
105 } else {
106 (self.cursor + self.bottom) as u32
107 }
108 }
109 pub fn cursor_prev(&mut self) -> bool {
117 if self.cursor_disabled {
118 return false;
119 }
120
121 if self.cursor <= self.scroll_padding() && self.bottom > 0 {
122 self.bottom -= 1;
123 } else if self.cursor > 0 {
124 self.cursor -= 1;
125 return self.cursor == 1;
126 } else if self.config.scroll_wrap {
127 self.cursor_jump(self.end());
128 }
129 false
130 }
131 pub fn cursor_next(&mut self) -> bool {
132 if self.cursor_disabled {
133 self.cursor_disabled = false
134 }
135
136 if self.cursor + 1 + self.scroll_padding() >= self.height
137 && self.bottom + self.height < self.status.matched_count as u16
138 {
139 self.bottom += 1;
140 } else if self.index() < self.end() {
141 self.cursor += 1;
142 if self.index() == self.end() {
143 return true;
144 }
145 } else if self.config.scroll_wrap {
146 self.cursor_jump(0)
147 }
148 false
149 }
150
151 pub fn cursor_jump(&mut self, index: u32) {
152 self.cursor_disabled = false;
153
154 let end = self.end();
155 let index = index.min(end) as u16;
156
157 if index < self.bottom || index >= self.bottom + self.height {
158 self.bottom = (end as u16 + 1).saturating_sub(self.height).min(index);
159 self.cursor = index - self.bottom;
160 } else {
161 self.cursor = index - self.bottom;
162 }
163 }
164
165 pub fn indentation(&self) -> usize {
167 self.config.multi_prefix.width()
168 }
169 pub fn col(&self) -> Option<usize> {
170 self.col
171 }
172 pub fn widths(&self) -> &Vec<u16> {
173 &self.widths
174 }
175 pub fn width(&self) -> u16 {
177 self.width.saturating_sub(self.indentation() as u16)
178 }
179 pub fn match_style(&self) -> Style {
180 Style::default()
181 .fg(self.config.match_fg)
182 .add_modifier(self.config.match_modifier)
183 }
184
185 pub fn max_widths(&self) -> Vec<u16> {
186 if !self.config.wrap {
187 return vec![];
188 }
189
190 let mut widths = vec![u16::MAX; self.widths.len()];
191
192 let total: u16 = self.widths.iter().sum();
193 if total <= self.width() {
194 return vec![];
195 }
196
197 let mut available = self.width();
198 let mut scale_total = 0;
199 let mut scalable_indices = Vec::new();
200
201 for (i, &w) in self.widths.iter().enumerate() {
202 if w <= 5 {
203 available = available.saturating_sub(w);
204 } else {
205 scale_total += w;
206 scalable_indices.push(i);
207 }
208 }
209
210 for &i in &scalable_indices {
211 let old = self.widths[i];
212 let new_w = old * available / scale_total;
213 widths[i] = new_w.max(5);
214 }
215
216 if let Some(&last_idx) = scalable_indices.last() {
218 let used_total: u16 = widths.iter().sum();
219 if used_total < self.width() {
220 widths[last_idx] += self.width() - used_total;
221 }
222 }
223
224 widths
225 }
226
227 pub fn make_table<'a, T: SSS>(
230 &'a mut self,
231 worker: &'a mut Worker<T>,
232 selections: &mut Selector<T, impl Selection>,
233 matcher: &mut nucleo::Matcher,
234 ) -> Table<'a> {
235 let offset = self.bottom as u32;
236 let end = (self.bottom + self.height) as u32;
237
238 let (mut results, mut widths, status) =
239 worker.results(offset, end, &self.max_widths(), self.match_style(), matcher);
240
241 let match_count = status.matched_count;
242
243 self.status = status;
244 if match_count < (self.bottom + self.cursor) as u32 && !self.cursor_disabled {
245 self.cursor_jump(match_count);
246 } else {
247 self.cursor = self.cursor.min(results.len().saturating_sub(1) as u16)
248 }
249
250 widths[0] += self.indentation() as u16;
251
252 let mut rows = vec![];
253 let mut total_height = 0;
254
255 if results.is_empty() {
256 return Table::new(rows, widths);
257 }
258
259 let cursor_result_h = results[self.cursor as usize].2;
261 let mut start_index = 0;
262
263 let cursor_should_above = self.height - self.scroll_padding();
264
265 if cursor_result_h >= cursor_should_above {
266 start_index = self.cursor;
267 self.bottom += self.cursor;
268 self.cursor = 0;
269 } else if let cursor_cum_h = results[0..=self.cursor as usize]
270 .iter()
271 .map(|(_, _, height)| height)
272 .sum::<u16>()
273 && cursor_cum_h > cursor_should_above
274 && self.bottom + self.height < self.status.matched_count as u16
275 {
276 start_index = 1;
277 let mut height = cursor_cum_h - cursor_should_above;
278 for (row, item, h) in results[..self.cursor as usize].iter_mut() {
279 let h = *h;
280
281 if height < h {
282 for (_, t) in row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0) {
283 clip_text_lines(t, height, !self.reverse());
284 }
285 total_height += height;
286
287 let prefix = if selections.contains(item) {
288 self.config.multi_prefix.clone().to_string()
289 } else {
290 fit_width(
291 &substitute_escaped(
292 &self.config.default_prefix,
293 &[
294 ('d', &(start_index - 1).to_string()),
295 ('r', &self.index().to_string()),
296 ],
297 ),
298 self.indentation(),
299 )
300 };
301
302 prefix_text(&mut row[0], prefix);
303
304 let last_visible = self
306 .config
307 .right_align_last
308 .then(|| {
309 widths
310 .iter()
311 .enumerate()
312 .rev()
313 .find(|(_, w)| **w != 0)
314 .map(|(i, _)| if i == 0 { None } else { Some(i) })
315 })
316 .flatten()
317 .flatten();
318
319 let row =
320 Row::new(row.iter().cloned().enumerate().filter_map(|(i, mut text)| {
321 (widths[i] != 0).then(|| {
322 if Some(i) == last_visible
323 && let Some(last_line) = text.lines.last_mut()
324 {
325 last_line.alignment = Some(Alignment::Right);
326 }
327 text
328 })
329 }))
330 .height(height);
331 rows.push(row);
334
335 self.bottom += start_index - 1;
336 self.cursor -= start_index - 1;
337 break;
338 } else if height == h {
339 self.bottom += start_index;
340 self.cursor -= start_index;
341 break;
343 }
344
345 start_index += 1;
346 height -= h;
347 }
348 }
349
350 for (i, (mut row, item, mut height)) in
353 (start_index..).zip(results.drain(start_index as usize..))
354 {
355 if self.height - total_height == 0 {
356 break;
357 } else if self.height - total_height < height {
358 height = self.height - total_height;
359
360 for (_, t) in row.iter_mut().enumerate().filter(|(i, _)| widths[*i] != 0) {
361 clip_text_lines(t, height, self.reverse());
362 }
363 total_height = self.height;
364 } else {
365 total_height += height;
366 }
367
368 let prefix = if selections.contains(item) {
369 self.config.multi_prefix.clone().to_string()
370 } else {
371 fit_width(
372 &substitute_escaped(
373 &self.config.default_prefix,
374 &[('d', &i.to_string()), ('r', &self.index().to_string())],
375 ),
376 self.indentation(),
377 )
378 };
379
380 prefix_text(&mut row[0], prefix);
381
382 if !self.cursor_disabled && i == self.cursor {
383 row = row
384 .into_iter()
385 .enumerate()
386 .map(|(i, t)| {
387 if self.col.is_none_or(|a| i == a) {
388 t.style(self.config.current_fg)
389 .bg(self.config.current_bg)
390 .add_modifier(self.config.current_modifier)
391 } else {
392 t
393 }
394 })
395 .collect();
396 }
397
398 let last_visible = self
400 .config
401 .right_align_last
402 .then(|| {
403 widths
404 .iter()
405 .enumerate()
406 .rev()
407 .find(|(_, w)| **w != 0)
408 .map(|(i, _)| if i == 0 { None } else { Some(i) })
409 })
410 .flatten()
411 .flatten();
412
413 let row = Row::new(row.iter().cloned().enumerate().filter_map(|(i, mut text)| {
415 (widths[i] != 0).then(|| {
416 if Some(i) == last_visible
417 && let Some(last_line) = text.lines.last_mut()
418 {
419 last_line.alignment = Some(Alignment::Right);
420 }
421 text
422 })
423 }))
424 .height(height);
425
426 rows.push(row);
427 }
428
429 if self.reverse() {
430 rows.reverse();
431 if total_height < self.height {
432 let spacer_height = self.height - total_height;
433 rows.insert(0, Row::new(vec![vec![]]).height(spacer_height));
434 }
435 }
436
437 self.widths = {
439 let pos = widths.iter().rposition(|&x| x != 0).map_or(0, |p| p + 1);
440 let mut widths = widths[..pos].to_vec();
441 if pos > 2 && self.config.right_align_last {
442 let used = widths.iter().take(widths.len() - 1).sum();
443 widths[pos - 1] = self.width().saturating_sub(used);
444 }
445 widths
446 };
447
448 let mut table = Table::new(rows, self.widths.clone())
449 .column_spacing(self.config.column_spacing.0)
450 .style(self.config.fg)
451 .add_modifier(self.config.modifier);
452
453 table = table.block(self.config.border.as_block());
454 table
455 }
456
457 pub fn make_status(&self) -> Paragraph<'_> {
458 Paragraph::new(format!(
459 " {}/{}",
460 &self.status.matched_count, &self.status.item_count
461 ))
462 .style(self.config.count_fg)
463 .add_modifier(self.config.count_modifier)
464 }
465}