flake_edit/tui/components/list/
model.rs1use std::collections::HashSet;
2
3use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ListAction {
8 Up,
9 Down,
10 Select,
11 ToggleDiff,
12 Cancel,
13 Toggle,
14 ToggleAll,
15 None,
16}
17
18impl ListAction {
19 pub fn from_key(key: KeyEvent) -> Self {
20 match key.code {
21 KeyCode::Up | KeyCode::Char('k') => ListAction::Up,
22 KeyCode::Down | KeyCode::Char('j') => ListAction::Down,
23 KeyCode::Enter => ListAction::Select,
24 KeyCode::Esc | KeyCode::Char('q') => ListAction::Cancel,
25 KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
26 ListAction::ToggleDiff
27 }
28 KeyCode::Char(' ') => ListAction::Toggle,
29 KeyCode::Char('u') | KeyCode::Char('U') => ListAction::ToggleAll,
30 _ => ListAction::None,
31 }
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum ListResult {
38 Select(Vec<usize>, bool),
40 Cancel,
41}
42
43#[derive(Debug, Clone)]
45pub struct ListState {
46 cursor: usize,
47 len: usize,
48 selected: HashSet<usize>,
49 show_diff: bool,
50 multi_select: bool,
51}
52
53impl ListState {
54 pub fn new(len: usize, multi_select: bool, initial_diff: bool) -> Self {
55 Self {
56 cursor: 0,
57 len,
58 selected: HashSet::new(),
59 show_diff: initial_diff,
60 multi_select,
61 }
62 }
63
64 pub fn cursor(&self) -> usize {
65 self.cursor
66 }
67
68 pub fn show_diff(&self) -> bool {
69 self.show_diff
70 }
71
72 pub fn is_selected(&self, index: usize) -> bool {
73 self.selected.contains(&index)
74 }
75
76 pub fn selected_count(&self) -> usize {
77 self.selected.len()
78 }
79
80 pub fn selected_indices(&self) -> Vec<usize> {
82 let mut indices: Vec<usize> = self.selected.iter().copied().collect();
83 indices.sort_unstable();
84 indices
85 }
86
87 pub fn multi_select(&self) -> bool {
88 self.multi_select
89 }
90
91 pub fn handle(&mut self, action: ListAction) -> Option<ListResult> {
93 match action {
94 ListAction::Up => {
95 self.cursor = if self.cursor == 0 {
96 self.len.saturating_sub(1)
97 } else {
98 self.cursor - 1
99 };
100 }
101 ListAction::Down => {
102 self.cursor = if self.cursor >= self.len.saturating_sub(1) {
103 0
104 } else {
105 self.cursor + 1
106 };
107 }
108 ListAction::Toggle if self.multi_select => {
109 if self.selected.contains(&self.cursor) {
110 self.selected.remove(&self.cursor);
111 } else {
112 self.selected.insert(self.cursor);
113 }
114 self.cursor = if self.cursor >= self.len.saturating_sub(1) {
116 0
117 } else {
118 self.cursor + 1
119 };
120 }
121 ListAction::ToggleAll if self.multi_select => {
122 if self.selected.len() == self.len {
123 self.selected.clear();
124 } else {
125 self.selected = (0..self.len).collect();
126 }
127 }
128 ListAction::ToggleDiff => {
129 self.show_diff = !self.show_diff;
130 }
131 ListAction::Select => {
132 if self.multi_select {
133 if self.selected.is_empty() {
134 return Some(ListResult::Cancel);
135 }
136 let mut indices: Vec<usize> = self.selected.iter().copied().collect();
137 indices.sort_unstable();
138 return Some(ListResult::Select(indices, self.show_diff));
139 } else {
140 return Some(ListResult::Select(vec![self.cursor], self.show_diff));
141 }
142 }
143 ListAction::Cancel => return Some(ListResult::Cancel),
144 _ => {}
145 }
146 None
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_single_select_navigation() {
156 let mut state = ListState::new(3, false, false);
157 assert_eq!(state.cursor(), 0);
158
159 state.handle(ListAction::Down);
160 assert_eq!(state.cursor(), 1);
161
162 state.handle(ListAction::Down);
163 assert_eq!(state.cursor(), 2);
164
165 state.handle(ListAction::Down);
166 assert_eq!(state.cursor(), 0); state.handle(ListAction::Up);
169 assert_eq!(state.cursor(), 2); }
171
172 #[test]
173 fn test_single_select_toggle_diff() {
174 let mut state = ListState::new(3, false, false);
175 assert!(!state.show_diff());
176
177 state.handle(ListAction::ToggleDiff);
178 assert!(state.show_diff());
179 }
180
181 #[test]
182 fn test_single_select_select() {
183 let mut state = ListState::new(3, false, true);
184 state.handle(ListAction::Down);
185 let result = state.handle(ListAction::Select);
186 assert_eq!(result, Some(ListResult::Select(vec![1], true)));
187 }
188
189 #[test]
190 fn test_single_select_ignores_toggle() {
191 let mut state = ListState::new(3, false, false);
192 state.handle(ListAction::Toggle);
193 assert_eq!(state.selected_count(), 0); assert_eq!(state.cursor(), 0); }
196
197 #[test]
198 fn test_multi_select_toggle() {
199 let mut state = ListState::new(3, true, false);
200 assert_eq!(state.selected_count(), 0);
201
202 state.handle(ListAction::Toggle);
203 assert_eq!(state.selected_count(), 1);
204 assert!(state.is_selected(0));
205 assert_eq!(state.cursor(), 1); }
207
208 #[test]
209 fn test_multi_select_toggle_all() {
210 let mut state = ListState::new(3, true, false);
211 state.handle(ListAction::ToggleAll);
212 assert_eq!(state.selected_count(), 3);
213
214 state.handle(ListAction::ToggleAll);
215 assert_eq!(state.selected_count(), 0);
216 }
217
218 #[test]
219 fn test_multi_select_submit_empty() {
220 let mut state = ListState::new(3, true, false);
221 let result = state.handle(ListAction::Select);
222 assert_eq!(result, Some(ListResult::Cancel)); }
224}