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