tui_prompts/
select_state.rs1use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
2
3use crate::prelude::*;
4
5#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
12pub struct SelectState {
13 status: Status,
14 focus: FocusState,
15 pub(crate) focused_index: usize,
16 pub(crate) option_count: usize,
17}
18
19impl SelectState {
20 #[must_use]
22 pub const fn new() -> Self {
23 Self {
24 status: Status::Pending,
25 focus: FocusState::Unfocused,
26 focused_index: 0,
27 option_count: 0,
28 }
29 }
30
31 pub fn move_up(&mut self) {
35 if self.focused_index > 0 {
36 self.focused_index -= 1;
37 }
38 }
39
40 pub fn move_down(&mut self) {
45 if self.focused_index < self.option_count.saturating_sub(1) {
46 self.focused_index += 1;
47 }
48 }
49
50 #[must_use]
52 pub const fn with_status(mut self, status: Status) -> Self {
53 self.status = status;
54 self
55 }
56
57 #[must_use]
59 pub const fn with_focus(mut self, focus: FocusState) -> Self {
60 self.focus = focus;
61 self
62 }
63
64 #[must_use]
66 pub const fn focused_index(&self) -> usize {
67 self.focused_index
68 }
69
70 pub fn set_focused_index(&mut self, index: usize) {
76 self.focused_index = self.clamp_focused_index(index);
77 }
78
79 #[must_use]
81 pub const fn is_finished(&self) -> bool {
82 self.status.is_finished()
83 }
84
85 #[must_use]
87 pub const fn status(&self) -> Status {
88 self.status
89 }
90
91 pub fn focus(&mut self) {
93 self.focus = FocusState::Focused;
94 }
95
96 pub fn blur(&mut self) {
98 self.focus = FocusState::Unfocused;
99 }
100
101 #[must_use]
103 pub fn is_focused(&self) -> bool {
104 self.focus == FocusState::Focused
105 }
106
107 pub fn complete(&mut self) {
109 self.status = Status::Done;
110 }
111
112 pub fn abort(&mut self) {
114 self.status = Status::Aborted;
115 }
116
117 pub fn handle_key_event(&mut self, key: KeyEvent) {
123 if key.kind == KeyEventKind::Release || self.status.is_finished() {
124 return;
125 }
126
127 match (key.code, key.modifiers) {
128 (KeyCode::Up, _) => self.move_up(),
129 (KeyCode::Down, _) => self.move_down(),
130 (KeyCode::Enter, _) if self.option_count > 0 => self.complete(),
131 (KeyCode::Esc, _) | (KeyCode::Char('c'), KeyModifiers::CONTROL) => self.abort(),
132 _ => {}
133 }
134 }
135
136 pub(crate) const fn clamp_focused_index(&self, index: usize) -> usize {
137 if self.option_count == 0 {
138 index
139 } else if index >= self.option_count {
140 self.option_count - 1
141 } else {
142 index
143 }
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 fn key(code: KeyCode, kind: KeyEventKind) -> KeyEvent {
152 KeyEvent::new_with_kind(code, KeyModifiers::NONE, kind)
153 }
154
155 fn ctrl_key(code: KeyCode) -> KeyEvent {
156 KeyEvent::new_with_kind(code, KeyModifiers::CONTROL, KeyEventKind::Press)
157 }
158
159 #[test]
160 fn render_option_count_clamps_focused_index() {
161 let mut state = SelectState::new();
162
163 state.set_focused_index(5);
164 state.option_count = 3;
165 state.focused_index = state.clamp_focused_index(state.focused_index);
166
167 assert_eq!(state.focused_index(), 2);
168 }
169
170 #[test]
171 fn set_focused_index_clamps_when_option_count_is_known() {
172 let mut state = SelectState::new();
173
174 state.option_count = 3;
175 state.set_focused_index(5);
176
177 assert_eq!(state.focused_index(), 2);
178 }
179
180 #[test]
181 fn move_down_stops_at_last_option() {
182 let mut state = SelectState::new();
183 state.option_count = 2;
184
185 state.move_down();
186 state.move_down();
187
188 assert_eq!(state.focused_index(), 1);
189 }
190
191 #[test]
192 fn handle_key_event_accepts_repeated_navigation() {
193 let mut state = SelectState::new();
194 state.option_count = 2;
195
196 state.handle_key_event(key(KeyCode::Down, KeyEventKind::Repeat));
197
198 assert_eq!(state.focused_index(), 1);
199 }
200
201 #[test]
202 fn handle_key_event_ignores_key_release() {
203 let mut state = SelectState::new();
204 state.option_count = 2;
205
206 state.handle_key_event(key(KeyCode::Down, KeyEventKind::Release));
207
208 assert_eq!(state.focused_index(), 0);
209 }
210
211 #[test]
212 fn handle_key_event_aborts_on_ctrl_c() {
213 let mut state = SelectState::new();
214
215 state.handle_key_event(ctrl_key(KeyCode::Char('c')));
216
217 assert_eq!(state.status(), Status::Aborted);
218 }
219
220 #[test]
221 fn handle_key_event_ignores_events_after_completion() {
222 let mut state = SelectState::new();
223 state.option_count = 2;
224 state.handle_key_event(key(KeyCode::Enter, KeyEventKind::Press));
225
226 state.handle_key_event(key(KeyCode::Down, KeyEventKind::Press));
227 state.handle_key_event(key(KeyCode::Esc, KeyEventKind::Press));
228
229 assert_eq!(state.focused_index(), 0);
230 assert_eq!(state.status(), Status::Done);
231 }
232
233 #[test]
234 fn handle_key_event_ignores_events_after_abort() {
235 let mut state = SelectState::new();
236 state.option_count = 2;
237 state.handle_key_event(key(KeyCode::Esc, KeyEventKind::Press));
238
239 state.handle_key_event(key(KeyCode::Enter, KeyEventKind::Press));
240
241 assert_eq!(state.status(), Status::Aborted);
242 }
243
244 #[test]
245 fn handle_key_event_does_not_complete_without_visible_options() {
246 let mut state = SelectState::new();
247
248 state.handle_key_event(key(KeyCode::Enter, KeyEventKind::Press));
249
250 assert_eq!(state.status(), Status::Pending);
251 }
252}