1use crate::widgets::column_widths::WidthChoice;
2use crate::widgets::text_input::TextInput;
3use ratatui::widgets::TableState;
4
5#[derive(Debug, Clone)]
6pub struct SortColumn {
7 pub name: String,
8 pub sort_order: Option<usize>, pub sort_descending: bool,
12 pub display_order: usize, pub is_locked: bool, pub is_to_be_locked: bool, pub is_visible: bool, pub width: WidthChoice,
18 pub shown_width: Option<u16>,
20}
21
22pub struct SortModal {
23 pub filter_input: TextInput,
24 pub columns: Vec<SortColumn>,
25 pub table_state: TableState,
26 pub has_unapplied_changes: bool,
27 pub history_limit: usize,
28 pub status: Option<String>,
31 pub applied_order: Vec<String>,
35 pub applied_locked: usize,
38 pub page_rows: usize,
40 shown: std::sync::Mutex<Option<(u64, Vec<usize>)>>,
43 #[cfg(test)]
45 shown_builds: std::sync::atomic::AtomicUsize,
46}
47
48impl Default for SortModal {
49 fn default() -> Self {
50 Self {
51 filter_input: TextInput::new(),
52 columns: Vec::new(),
53 table_state: TableState::default(),
54 has_unapplied_changes: false,
55 history_limit: 1000,
56 status: None,
57 applied_order: Vec::new(),
58 applied_locked: 0,
59 page_rows: 10,
60 shown: Default::default(),
61 #[cfg(test)]
62 shown_builds: Default::default(),
63 }
64 }
65}
66
67impl SortModal {
68 pub fn filtered_columns(&self) -> Vec<(usize, &SortColumn)> {
71 use std::hash::{Hash, Hasher};
72 let mut hasher = std::collections::hash_map::DefaultHasher::new();
73 self.filter_input.value().hash(&mut hasher);
74 for c in &self.columns {
75 (&c.name, c.display_order).hash(&mut hasher);
76 }
77 let key = hasher.finish();
78 let mut shown = self.shown.lock().unwrap_or_else(|e| e.into_inner());
79 if shown.as_ref().is_none_or(|(at, _)| *at != key) {
80 let filter_text = self.filter_input.value().to_lowercase();
81 let mut filtered: Vec<usize> = (0..self.columns.len())
82 .filter(|&i| self.columns[i].name.to_lowercase().contains(&filter_text))
83 .collect();
84 filtered.sort_by_key(|&i| self.columns[i].display_order);
85 *shown = Some((key, filtered));
86 #[cfg(test)]
87 self.shown_builds
88 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
89 }
90 let rows = shown.as_ref().map_or(&[][..], |(_, rows)| rows);
91 rows.iter().map(|&i| (i, &self.columns[i])).collect()
92 }
93
94 pub fn get_column_order(&self) -> Vec<String> {
95 let mut cols: Vec<_> = self.columns.iter().filter(|c| c.is_visible).collect();
96 cols.sort_by_key(|c| c.display_order);
97 cols.into_iter().map(|c| c.name.clone()).collect()
98 }
99
100 pub fn get_locked_columns_count(&self) -> usize {
104 let Some(last_locked) = self
105 .columns
106 .iter()
107 .filter(|c| c.is_locked)
108 .map(|c| c.display_order)
109 .max()
110 else {
111 return 0;
112 };
113 self.columns
114 .iter()
115 .filter(|c| c.is_visible && c.display_order <= last_locked)
116 .count()
117 }
118
119 pub fn get_locked_span(&self) -> usize {
121 self.columns
122 .iter()
123 .filter(|c| c.is_locked)
124 .map(|c| c.display_order + 1)
125 .max()
126 .unwrap_or(0)
127 }
128
129 pub fn get_full_column_order(&self) -> Vec<String> {
131 let mut cols: Vec<_> = self.columns.iter().collect();
132 cols.sort_by_key(|c| c.display_order);
133 cols.into_iter().map(|c| c.name.clone()).collect()
134 }
135
136 pub fn get_sorted_columns(&self) -> Vec<String> {
137 self.sorted_columns_and_directions().0
138 }
139
140 pub fn sorted_columns_and_directions(&self) -> (Vec<String>, Vec<bool>) {
143 let mut sorted: Vec<_> = self
144 .columns
145 .iter()
146 .filter_map(|c| c.sort_order.map(|o| (o, c.name.clone(), c.sort_descending)))
147 .collect();
148 sorted.sort_by_key(|(order, _, _)| *order);
149 sorted
150 .into_iter()
151 .map(|(_, name, descending)| (name, descending))
152 .unzip()
153 }
154
155 pub fn cycle_sort(&mut self) {
158 if let Some(idx) = self.table_state.selected() {
159 let filtered = self.filtered_columns();
160 if let Some((real_idx, _)) = filtered.get(idx) {
161 let real_idx = *real_idx;
162 match (
163 self.columns[real_idx].sort_order,
164 self.columns[real_idx].sort_descending,
165 ) {
166 (None, _) => {
167 let max_order = self
168 .columns
169 .iter()
170 .filter_map(|c| c.sort_order)
171 .max()
172 .unwrap_or(0);
173 self.columns[real_idx].sort_order = Some(max_order + 1);
174 self.columns[real_idx].sort_descending = false;
175 }
176 (Some(_), false) => {
177 self.columns[real_idx].sort_descending = true;
178 }
179 (Some(_), true) => self.unsort_index(real_idx),
180 }
181 self.has_unapplied_changes = true;
182 }
183 }
184 }
185
186 pub fn cycle_sort_back(&mut self) {
189 let Some(idx) = self.table_state.selected() else {
190 return;
191 };
192 let Some(&(real_idx, _)) = self.filtered_columns().get(idx) else {
193 return;
194 };
195 let col = &self.columns[real_idx];
196 match (col.sort_order, col.sort_descending) {
197 (None, _) => {
198 let max_order = self.columns.iter().filter_map(|c| c.sort_order).max();
199 self.columns[real_idx].sort_order = Some(max_order.unwrap_or(0) + 1);
200 self.columns[real_idx].sort_descending = true;
201 }
202 (Some(_), true) => self.columns[real_idx].sort_descending = false,
203 (Some(_), false) => self.unsort_index(real_idx),
204 }
205 self.has_unapplied_changes = true;
206 }
207
208 pub fn sort_entries(&self) -> Vec<usize> {
210 let mut entries: Vec<(usize, usize)> = self
211 .columns
212 .iter()
213 .enumerate()
214 .filter_map(|(i, c)| c.sort_order.map(|o| (o, i)))
215 .collect();
216 entries.sort_unstable();
217 entries.into_iter().map(|(_, i)| i).collect()
218 }
219
220 pub fn flip_sort(&mut self, entry: usize) {
222 if let Some(&i) = self.sort_entries().get(entry) {
223 self.columns[i].sort_descending = !self.columns[i].sort_descending;
224 self.has_unapplied_changes = true;
225 }
226 }
227
228 pub fn remove_sort_entry(&mut self, entry: usize) {
230 if let Some(&i) = self.sort_entries().get(entry) {
231 self.unsort_index(i);
232 self.has_unapplied_changes = true;
233 }
234 }
235
236 pub fn move_sort_entry(&mut self, entry: usize, earlier: bool) -> usize {
239 let entries = self.sort_entries();
240 let to = if earlier {
241 entry.checked_sub(1)
242 } else {
243 Some(entry + 1).filter(|to| *to < entries.len())
244 };
245 let (Some(to), Some(&from_i)) = (to, entries.get(entry)) else {
246 return entry;
247 };
248 let to_i = entries[to];
249 let a = self.columns[from_i].sort_order;
250 self.columns[from_i].sort_order = self.columns[to_i].sort_order;
251 self.columns[to_i].sort_order = a;
252 self.has_unapplied_changes = true;
253 to
254 }
255
256 pub fn add_sort(&mut self, name: &str) -> Option<usize> {
260 let i = self.columns.iter().position(|c| c.name == name)?;
261 if self.columns[i].sort_order.is_none() {
262 let max_order = self.columns.iter().filter_map(|c| c.sort_order).max();
263 self.columns[i].sort_order = Some(max_order.unwrap_or(0) + 1);
264 self.columns[i].sort_descending = false;
265 self.has_unapplied_changes = true;
266 }
267 self.sort_entries().iter().position(|&e| e == i)
268 }
269
270 pub fn remove_sort(&mut self) {
273 if let Some(idx) = self.table_state.selected() {
274 let filtered = self.filtered_columns();
275 if let Some((real_idx, _)) = filtered.get(idx) {
276 let real_idx = *real_idx;
277 if self.columns[real_idx].sort_order.is_some() {
278 self.unsort_index(real_idx);
279 self.has_unapplied_changes = true;
280 }
281 }
282 }
283 }
284
285 fn unsort_index(&mut self, real_idx: usize) {
286 let Some(old_order) = self.columns[real_idx].sort_order else {
287 return;
288 };
289 self.columns[real_idx].sort_order = None;
290 self.columns[real_idx].sort_descending = false;
291 for col in &mut self.columns {
292 if let Some(order) = col.sort_order
293 && order > old_order
294 {
295 col.sort_order = Some(order - 1);
296 }
297 }
298 }
299
300 pub fn move_column_display_up(&mut self) {
302 if let Some(idx) = self.table_state.selected() {
303 let filtered = self.filtered_columns();
304 if let Some((real_idx, _)) = filtered.get(idx) {
305 let real_idx = *real_idx;
306 let current_display_order = self.columns[real_idx].display_order;
307 let was_locked = self.columns[real_idx].is_locked;
308 if current_display_order > 0 {
309 let mut target_col_locked = false;
311 let mut target_col_to_be_locked = false;
312 for col in &self.columns {
313 if col.display_order == current_display_order - 1 {
314 target_col_locked = col.is_locked;
315 target_col_to_be_locked = col.is_to_be_locked;
316 break;
317 }
318 }
319
320 for col in &mut self.columns {
322 if col.display_order == current_display_order - 1 {
323 col.display_order = current_display_order;
324 break;
325 }
326 }
327 let new_display_order = current_display_order - 1;
328 let was_to_be_locked = self.columns[real_idx].is_to_be_locked;
329
330 let last_locked_or_to_be_order = self
333 .columns
334 .iter()
335 .filter(|c| c.is_locked || c.is_to_be_locked)
336 .map(|c| c.display_order)
337 .max()
338 .unwrap_or(0);
339
340 self.columns[real_idx].display_order = new_display_order;
342
343 if !was_locked && !was_to_be_locked {
345 if target_col_locked || target_col_to_be_locked {
346 self.columns[real_idx].is_locked = target_col_locked;
348 self.columns[real_idx].is_to_be_locked = target_col_to_be_locked;
349 }
350 } else {
351 if (was_locked || was_to_be_locked)
354 && current_display_order == last_locked_or_to_be_order
355 {
356 for col in &mut self.columns {
359 if col.display_order > new_display_order
360 && col.display_order <= current_display_order
361 && col.is_to_be_locked
362 {
363 col.is_to_be_locked = false;
364 }
365 }
366 }
367 }
368
369 self.has_unapplied_changes = true;
370 if let Some(new_selected_idx) = self
371 .filtered_columns()
372 .iter()
373 .position(|&(idx, _)| idx == real_idx)
374 {
375 self.table_state.select(Some(new_selected_idx));
376 }
377 }
378 }
379 }
380 }
381
382 pub fn move_column_display_down(&mut self) {
384 if let Some(idx) = self.table_state.selected() {
385 let filtered = self.filtered_columns();
386 if let Some((real_idx, _)) = filtered.get(idx) {
387 let real_idx = *real_idx;
388 let max_display_order = self
389 .columns
390 .iter()
391 .map(|c| c.display_order)
392 .max()
393 .unwrap_or(0);
394 let current_display_order = self.columns[real_idx].display_order;
395 let was_locked = self.columns[real_idx].is_locked;
396 if current_display_order < max_display_order {
397 for col in &mut self.columns {
399 if col.display_order == current_display_order + 1 {
400 col.display_order = current_display_order;
401 break;
402 }
403 }
404 let new_display_order = current_display_order + 1;
405 let was_to_be_locked = self.columns[real_idx].is_to_be_locked;
406
407 self.columns[real_idx].display_order = new_display_order;
409
410 if was_locked || was_to_be_locked {
413 for (idx, col) in self.columns.iter_mut().enumerate() {
414 if idx != real_idx
417 && col.display_order >= current_display_order
418 && col.display_order < new_display_order
419 && !col.is_locked
420 {
421 col.is_to_be_locked = true;
422 }
423 }
424 }
425
426 self.has_unapplied_changes = true;
427 if let Some(new_selected_idx) = self
428 .filtered_columns()
429 .iter()
430 .position(|&(idx, _)| idx == real_idx)
431 {
432 self.table_state.select(Some(new_selected_idx));
433 }
434 }
435 }
436 }
437 }
438
439 pub fn toggle_lock_at_column(&mut self) {
441 if let Some(idx) = self.table_state.selected() {
442 let filtered = self.filtered_columns();
443 if let Some((real_idx, _)) = filtered.get(idx) {
444 let real_idx = *real_idx;
445 let target_display_order = self.columns[real_idx].display_order;
446
447 let current_locked_count = self.columns.iter().filter(|c| c.is_locked).count();
449
450 if target_display_order < current_locked_count {
452 for col in &mut self.columns {
454 col.is_locked = col.display_order < target_display_order;
455 col.is_to_be_locked = false; }
457 } else {
458 for col in &mut self.columns {
460 col.is_locked = col.display_order <= target_display_order;
461 col.is_to_be_locked = false; }
463 }
464 self.has_unapplied_changes = true;
465 }
466 }
467 }
468
469 pub fn move_selection_up(&mut self) {
470 if let Some(idx) = self.table_state.selected() {
471 let filtered = self.filtered_columns();
472 if let Some((real_idx, _)) = filtered.get(idx) {
473 let real_idx = *real_idx;
474 if let Some(current_order) = self.columns[real_idx].sort_order
475 && current_order > 1
476 {
477 for col in &mut self.columns {
478 if col.sort_order == Some(current_order - 1) {
479 col.sort_order = Some(current_order);
480 break;
481 }
482 }
483 self.columns[real_idx].sort_order = Some(current_order - 1);
484 self.has_unapplied_changes = true;
485 if let Some(new_selected_idx) = self
486 .filtered_columns()
487 .iter()
488 .position(|&(idx, _)| idx == real_idx)
489 {
490 self.table_state.select(Some(new_selected_idx));
491 }
492 }
493 }
494 }
495 }
496
497 pub fn move_selection_down(&mut self) {
498 if let Some(idx) = self.table_state.selected() {
499 let filtered = self.filtered_columns();
500 if let Some((real_idx, _)) = filtered.get(idx) {
501 let real_idx = *real_idx;
502 let max_order = self
503 .columns
504 .iter()
505 .filter_map(|c| c.sort_order)
506 .max()
507 .unwrap_or(0);
508 if let Some(current_order) = self.columns[real_idx].sort_order
509 && current_order < max_order
510 {
511 for col in &mut self.columns {
512 if col.sort_order == Some(current_order + 1) {
513 col.sort_order = Some(current_order);
514 break;
515 }
516 }
517 self.columns[real_idx].sort_order = Some(current_order + 1);
518 if let Some(new_selected_idx) = self
519 .filtered_columns()
520 .iter()
521 .position(|&(idx, _)| idx == real_idx)
522 {
523 self.table_state.select(Some(new_selected_idx));
524 }
525 self.has_unapplied_changes = true;
526 }
527 }
528 }
529 }
530
531 pub fn clear_selection(&mut self) {
532 for (idx, col) in self.columns.iter_mut().enumerate() {
533 col.sort_order = None;
534 col.sort_descending = false;
535 col.is_locked = false;
536 col.is_to_be_locked = false;
537 col.display_order = idx; col.is_visible = true; col.width = WidthChoice::Auto;
540 }
541 self.has_unapplied_changes = true;
542 }
543
544 pub fn change_width(&mut self, change: impl FnOnce(WidthChoice, Option<u16>) -> WidthChoice) {
548 let Some(idx) = self.table_state.selected() else {
549 return;
550 };
551 let Some(&(real_idx, _)) = self.filtered_columns().get(idx) else {
552 return;
553 };
554 let col = &mut self.columns[real_idx];
555 let width = change(col.width, col.shown_width);
556 if width != col.width {
557 col.width = width;
558 self.has_unapplied_changes = true;
559 }
560 }
561
562 pub fn width_choices(&self) -> Vec<(String, WidthChoice)> {
564 self.columns
565 .iter()
566 .map(|c| (c.name.clone(), c.width))
567 .collect()
568 }
569
570 pub fn toggle_visibility(&mut self) {
573 if let Some(idx) = self.table_state.selected() {
574 let filtered = self.filtered_columns();
575 if let Some(&(real_idx, _)) = filtered.get(idx) {
576 let col = &mut self.columns[real_idx];
577 col.is_visible = !col.is_visible;
578 self.has_unapplied_changes = true;
579 }
580 }
581 }
582
583 pub fn jump_selection_to_order(&mut self, new_order: usize) {
584 if let Some(idx) = self.table_state.selected() {
585 let filtered = self.filtered_columns();
586 if let Some((real_idx, _)) = filtered.get(idx) {
587 let real_idx = *real_idx;
588 let max_order = self
589 .columns
590 .iter()
591 .filter_map(|c| c.sort_order)
592 .max()
593 .unwrap_or(0);
594 let old_order = self.columns[real_idx].sort_order;
595 let last = if old_order.is_some() {
598 max_order
599 } else {
600 max_order + 1
601 };
602
603 if new_order > 0 && new_order <= last {
604 let selected_column_name = self.columns[real_idx].name.clone();
605
606 for col in &mut self.columns {
608 if col.name == selected_column_name {
609 continue; }
611 if let Some(order) = col.sort_order {
612 if let Some(old) = old_order {
613 if new_order < old && order >= new_order && order < old {
614 col.sort_order = Some(order + 1);
615 } else if new_order > old && order <= new_order && order > old {
616 col.sort_order = Some(order - 1);
617 }
618 } else {
619 if order >= new_order {
621 col.sort_order = Some(order + 1);
622 }
623 }
624 }
625 }
626 self.columns[real_idx].sort_order = Some(new_order);
627
628 let mut current_sorted_cols: Vec<(&mut SortColumn, usize)> = self
630 .columns
631 .iter_mut()
632 .filter_map(|c| c.sort_order.map(|o| (c, o)))
633 .collect();
634 current_sorted_cols.sort_by_key(|(_, o)| *o);
635
636 for (i, (col, _)) in current_sorted_cols.into_iter().enumerate() {
637 col.sort_order = Some(i + 1);
638 }
639
640 if let Some(new_selected_idx) = self
641 .filtered_columns()
642 .iter()
643 .position(|&(r_idx, _)| r_idx == real_idx)
644 {
645 self.table_state.select(Some(new_selected_idx));
646 }
647 if old_order != Some(new_order) {
648 self.has_unapplied_changes = true;
649 }
650 } else if new_order == 0 {
651 if self.columns[real_idx].sort_order.take().is_some() {
653 self.has_unapplied_changes = true;
654 }
655 let mut current_sorted_cols: Vec<(&mut SortColumn, usize)> = self
657 .columns
658 .iter_mut()
659 .filter_map(|c| c.sort_order.map(|o| (c, o)))
660 .collect();
661 current_sorted_cols.sort_by_key(|(_, o)| *o);
662
663 for (i, (col, _)) in current_sorted_cols.into_iter().enumerate() {
664 col.sort_order = Some(i + 1);
665 }
666 if let Some(new_selected_idx) = self
668 .filtered_columns()
669 .iter()
670 .position(|&(r_idx, _)| r_idx == real_idx)
671 {
672 self.table_state.select(Some(new_selected_idx));
673 }
674 } else {
675 let range = if last == 1 {
677 "1".to_string()
678 } else {
679 format!("1-{last}")
680 };
681 self.status = Some(format!(
682 "Position {new_order} is past the end; use {range}."
683 ));
684 }
685 }
686 }
687 }
688}
689
690pub fn order_with_hidden(visible: &[String], all: &[String], reference: &[String]) -> Vec<String> {
695 use std::collections::HashSet;
696 let mut order = visible.to_vec();
697 let mut placed: HashSet<&str> = visible.iter().map(String::as_str).collect();
698 for name in all {
699 if placed.contains(name.as_str()) {
700 continue;
701 }
702 let earlier = match reference.iter().position(|r| r == name) {
703 Some(i) => &reference[..i],
704 None => &all[..all.iter().position(|a| a == name).unwrap_or(0)],
705 };
706 let at = earlier
707 .iter()
708 .rev()
709 .find(|e| placed.contains(e.as_str()))
710 .and_then(|e| order.iter().position(|o| o == e))
711 .map_or(0, |p| p + 1);
712 order.insert(at, name.clone());
713 placed.insert(name.as_str());
714 }
715 order
716}
717
718#[cfg(test)]
719mod tests;