1use crate::widgets::ui::PickerState;
22use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum FieldKind {
27 Text,
30 MultilineText,
33 Choice,
35 Checkbox,
37 Picker { multi: bool },
41 Button,
43}
44
45impl FieldKind {
46 pub fn types(self) -> bool {
48 matches!(self, Self::Text | Self::MultilineText)
49 }
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum FormKey<F> {
55 Moved,
57 Submit,
59 Cancel,
61 Step(F, i8),
64 Act(F),
66 Text(F),
68 Other,
70}
71
72pub trait Form {
75 type Field: Copy + Eq + std::fmt::Debug;
77
78 fn fields(&self) -> Vec<(Self::Field, FieldKind)>;
81
82 fn focused(&self) -> Self::Field;
83
84 fn set_focused(&mut self, field: Self::Field);
87
88 fn text_edge(&self, _field: Self::Field) -> (bool, bool) {
91 (true, true)
92 }
93
94 fn list_row(&self, _field: Self::Field) -> bool {
99 false
100 }
101
102 fn focused_kind(&self) -> Option<FieldKind> {
104 let focused = self.focused();
105 self.fields()
106 .into_iter()
107 .find(|(field, _)| *field == focused)
108 .map(|(_, kind)| kind)
109 }
110
111 fn focus(&mut self, field: Self::Field) -> bool {
114 let shown = self.fields().iter().any(|(f, _)| *f == field);
115 if shown {
116 self.set_focused(field);
117 }
118 shown
119 }
120
121 fn move_focus(&mut self, delta: isize) {
124 let fields = self.fields();
125 if fields.is_empty() {
126 return;
127 }
128 let focused = self.focused();
129 let n = fields.len() as isize;
130 let next = match fields.iter().position(|(f, _)| *f == focused) {
131 Some(at) => (at as isize + delta).rem_euclid(n),
132 None if delta < 0 => n - 1,
133 None => 0,
134 };
135 self.set_focused(fields[next as usize].0);
136 }
137
138 fn focus_next(&mut self) {
139 self.move_focus(1);
140 }
141
142 fn focus_prev(&mut self) {
143 self.move_focus(-1);
144 }
145
146 fn settle_focus(&mut self) {
149 if self.focused_kind().is_none()
150 && let Some((first, _)) = self.fields().first().copied()
151 {
152 self.set_focused(first);
153 }
154 }
155}
156
157fn plain(event: &KeyEvent) -> bool {
158 event
159 .modifiers
160 .intersection(KeyModifiers::CONTROL | KeyModifiers::ALT)
161 .is_empty()
162}
163
164pub fn key<T: Form + ?Sized>(form: &mut T, event: &KeyEvent) -> FormKey<T::Field> {
167 let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
168 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
169 let Some(kind) = form.focused_kind() else {
170 form.settle_focus();
173 return match event.code {
174 KeyCode::Esc => FormKey::Cancel,
175 KeyCode::Enter => FormKey::Submit,
176 _ => FormKey::Other,
177 };
178 };
179 let field = form.focused();
180 let code = match event.code {
182 KeyCode::Char('h') if !kind.types() && plain(event) => KeyCode::Left,
183 KeyCode::Char('j') if !kind.types() && plain(event) => KeyCode::Down,
184 KeyCode::Char('k') if !kind.types() && plain(event) => KeyCode::Up,
185 KeyCode::Char('l') if !kind.types() && plain(event) => KeyCode::Right,
186 code => code,
187 };
188 match code {
189 KeyCode::Esc => FormKey::Cancel,
190 KeyCode::Enter | KeyCode::Char('j' | 'J') if ctrl => FormKey::Submit,
192 KeyCode::Enter if kind == FieldKind::MultilineText => FormKey::Text(field),
193 KeyCode::Enter => FormKey::Submit,
194 KeyCode::BackTab => {
195 form.focus_prev();
196 FormKey::Moved
197 }
198 KeyCode::Tab if shift => {
199 form.focus_prev();
200 FormKey::Moved
201 }
202 KeyCode::Tab => {
203 form.focus_next();
204 FormKey::Moved
205 }
206 KeyCode::Up | KeyCode::Down if event.modifiers.is_empty() => {
207 let up = code == KeyCode::Up;
208 if kind == FieldKind::MultilineText {
209 let (first, last) = form.text_edge(field);
210 if (up && !first) || (!up && !last) {
211 return FormKey::Text(field);
212 }
213 }
214 form.move_focus(if up { -1 } else { 1 });
215 FormKey::Moved
216 }
217 KeyCode::Left | KeyCode::Right => {
218 let delta = if code == KeyCode::Left { -1 } else { 1 };
219 match kind {
220 FieldKind::Text | FieldKind::MultilineText => FormKey::Text(field),
221 FieldKind::Choice | FieldKind::Picker { multi: false } => {
222 FormKey::Step(field, delta)
223 }
224 FieldKind::Checkbox => FormKey::Act(field),
225 FieldKind::Picker { multi: true } | FieldKind::Button => FormKey::Other,
226 }
227 }
228 KeyCode::Char(' ') if plain(event) => match kind {
229 FieldKind::Text | FieldKind::MultilineText => FormKey::Text(field),
230 FieldKind::Choice => FormKey::Step(field, 1),
231 FieldKind::Checkbox | FieldKind::Picker { .. } | FieldKind::Button => {
232 FormKey::Act(field)
233 }
234 },
235 _ if kind.types() => FormKey::Text(field),
236 _ => FormKey::Other,
237 }
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
242pub enum PickerKey {
243 Handled,
245 Close,
247 Choose,
249 Toggle,
251 ChooseAndMove(bool),
253 Other,
255}
256
257pub fn picker_key(picker: &mut PickerState, multi: bool, event: &KeyEvent) -> PickerKey {
262 let shift = event.modifiers.contains(KeyModifiers::SHIFT);
263 match event.code {
264 KeyCode::Esc => PickerKey::Close,
265 KeyCode::Enter => PickerKey::Choose,
266 KeyCode::BackTab => PickerKey::ChooseAndMove(false),
267 KeyCode::Tab => PickerKey::ChooseAndMove(!shift),
268 KeyCode::Up => {
269 picker.move_up();
270 PickerKey::Handled
271 }
272 KeyCode::Down => {
273 picker.move_down();
274 PickerKey::Handled
275 }
276 KeyCode::Char(' ') if multi => PickerKey::Toggle,
277 KeyCode::Char(' ') => PickerKey::Choose,
278 KeyCode::Backspace => {
279 picker.backspace();
280 PickerKey::Handled
281 }
282 KeyCode::Char(c) => {
283 picker.filter_key(c, event.modifiers);
284 PickerKey::Handled
285 }
286 _ => PickerKey::Other,
287 }
288}
289
290pub fn step_index(at: usize, len: usize, delta: i8) -> usize {
292 if len == 0 {
293 return 0;
294 }
295 (at as isize + delta as isize).rem_euclid(len as isize) as usize
296}
297
298pub fn step_value<T: Copy + PartialEq>(all: &[T], current: T, delta: i8) -> T {
301 let at = all.iter().position(|v| *v == current);
302 match at {
303 Some(at) => all[step_index(at, all.len(), delta)],
304 None => all[0],
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
313 enum F {
314 Name,
315 Notes,
316 Format,
317 Header,
318 Column,
319 Tags,
320 Go,
321 }
322
323 struct T {
324 focus: F,
325 show_header: bool,
326 edge: (bool, bool),
327 }
328
329 impl Form for T {
330 type Field = F;
331 fn fields(&self) -> Vec<(F, FieldKind)> {
332 let mut fields = vec![
333 (F::Name, FieldKind::Text),
334 (F::Notes, FieldKind::MultilineText),
335 (F::Format, FieldKind::Choice),
336 ];
337 if self.show_header {
338 fields.push((F::Header, FieldKind::Checkbox));
339 }
340 fields.extend([
341 (F::Column, FieldKind::Picker { multi: false }),
342 (F::Tags, FieldKind::Picker { multi: true }),
343 (F::Go, FieldKind::Button),
344 ]);
345 fields
346 }
347 fn focused(&self) -> F {
348 self.focus
349 }
350 fn set_focused(&mut self, field: F) {
351 self.focus = field;
352 }
353 fn text_edge(&self, _: F) -> (bool, bool) {
354 self.edge
355 }
356 }
357
358 fn form() -> T {
359 T {
360 focus: F::Name,
361 show_header: true,
362 edge: (true, true),
363 }
364 }
365
366 fn press(code: KeyCode) -> KeyEvent {
367 KeyEvent::new(code, KeyModifiers::NONE)
368 }
369
370 fn ctrl(c: char) -> KeyEvent {
371 KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL)
372 }
373
374 #[test]
375 fn tab_and_the_arrows_walk_every_field_and_wrap() {
376 let mut f = form();
377 let mut walked = vec![f.focus];
378 for _ in 0..7 {
379 assert_eq!(key(&mut f, &press(KeyCode::Down)), FormKey::Moved);
380 walked.push(f.focus);
381 }
382 assert_eq!(
383 walked,
384 [
385 F::Name,
386 F::Notes,
387 F::Format,
388 F::Header,
389 F::Column,
390 F::Tags,
391 F::Go,
392 F::Name
393 ]
394 );
395 key(&mut f, &press(KeyCode::Up));
396 assert_eq!(f.focus, F::Go, "↑ from the first field wraps to the last");
397 key(&mut f, &press(KeyCode::Tab));
398 assert_eq!(f.focus, F::Name);
399 key(&mut f, &press(KeyCode::BackTab));
400 assert_eq!(f.focus, F::Go);
401 key(&mut f, &KeyEvent::new(KeyCode::Tab, KeyModifiers::SHIFT));
402 assert_eq!(
403 f.focus,
404 F::Tags,
405 "Shift+Tab as Tab with Shift goes back too"
406 );
407 }
408
409 #[test]
410 fn hidden_fields_are_skipped_and_a_hidden_focus_settles() {
411 let mut f = form();
412 f.show_header = false;
413 f.focus = F::Format;
414 key(&mut f, &press(KeyCode::Down));
415 assert_eq!(f.focus, F::Column);
416 f.focus = F::Header;
417 f.settle_focus();
418 assert_eq!(f.focus, F::Name);
419 assert!(!f.focus(F::Header), "a hidden field cannot take focus");
420 assert!(f.focus(F::Tags));
421 assert_eq!(f.focus, F::Tags);
422 }
423
424 #[test]
425 fn the_arrows_across_step_choices_and_edit_text() {
426 let mut f = form();
427 assert_eq!(
428 key(&mut f, &press(KeyCode::Left)),
429 FormKey::Text(F::Name),
430 "a text field keeps ← for its cursor"
431 );
432 f.focus = F::Format;
433 assert_eq!(
434 key(&mut f, &press(KeyCode::Left)),
435 FormKey::Step(F::Format, -1)
436 );
437 assert_eq!(
438 key(&mut f, &press(KeyCode::Right)),
439 FormKey::Step(F::Format, 1)
440 );
441 f.focus = F::Column;
442 assert_eq!(
443 key(&mut f, &press(KeyCode::Right)),
444 FormKey::Step(F::Column, 1)
445 );
446 f.focus = F::Tags;
447 assert_eq!(key(&mut f, &press(KeyCode::Right)), FormKey::Other);
448 f.focus = F::Header;
449 assert_eq!(key(&mut f, &press(KeyCode::Right)), FormKey::Act(F::Header));
450 }
451
452 #[test]
453 fn space_acts_on_the_field() {
454 let mut f = form();
455 let space = press(KeyCode::Char(' '));
456 assert_eq!(key(&mut f, &space), FormKey::Text(F::Name), "text types it");
457 f.focus = F::Format;
458 assert_eq!(key(&mut f, &space), FormKey::Step(F::Format, 1));
459 for field in [F::Header, F::Column, F::Tags, F::Go] {
460 f.focus = field;
461 assert_eq!(key(&mut f, &space), FormKey::Act(field));
462 }
463 }
464
465 #[test]
466 fn enter_submits_from_any_field_but_types_in_a_multiline_one() {
467 let mut f = form();
468 for field in [F::Name, F::Format, F::Header, F::Column, F::Tags, F::Go] {
469 f.focus = field;
470 assert_eq!(key(&mut f, &press(KeyCode::Enter)), FormKey::Submit);
471 }
472 f.focus = F::Notes;
473 assert_eq!(key(&mut f, &press(KeyCode::Enter)), FormKey::Text(F::Notes));
474 assert_eq!(key(&mut f, &ctrl('j')), FormKey::Submit);
475 assert_eq!(
476 key(
477 &mut f,
478 &KeyEvent::new(KeyCode::Enter, KeyModifiers::CONTROL)
479 ),
480 FormKey::Submit
481 );
482 }
483
484 #[test]
485 fn esc_cancels_from_any_field() {
486 let mut f = form();
487 for field in [F::Name, F::Notes, F::Format, F::Go] {
488 f.focus = field;
489 assert_eq!(key(&mut f, &press(KeyCode::Esc)), FormKey::Cancel);
490 }
491 }
492
493 #[test]
494 fn history_is_ctrl_p_and_n_and_the_arrows_never_recall() {
495 let mut f = form();
496 assert_eq!(key(&mut f, &ctrl('p')), FormKey::Text(F::Name));
497 assert_eq!(key(&mut f, &ctrl('n')), FormKey::Text(F::Name));
498 assert_eq!(key(&mut f, &press(KeyCode::Up)), FormKey::Moved);
499 assert_eq!(f.focus, F::Go);
500 }
501
502 #[test]
503 fn a_multiline_field_keeps_the_arrows_until_its_edge() {
504 let mut f = form();
505 f.focus = F::Notes;
506 f.edge = (false, false);
507 assert_eq!(key(&mut f, &press(KeyCode::Up)), FormKey::Text(F::Notes));
508 assert_eq!(key(&mut f, &press(KeyCode::Down)), FormKey::Text(F::Notes));
509 f.edge = (true, false);
510 assert_eq!(key(&mut f, &press(KeyCode::Up)), FormKey::Moved);
511 assert_eq!(f.focus, F::Name);
512 }
513
514 #[test]
515 fn vim_keys_move_only_where_nothing_types() {
516 let mut f = form();
517 assert_eq!(
518 key(&mut f, &press(KeyCode::Char('j'))),
519 FormKey::Text(F::Name)
520 );
521 f.focus = F::Format;
522 assert_eq!(
523 key(&mut f, &press(KeyCode::Char('l'))),
524 FormKey::Step(F::Format, 1)
525 );
526 key(&mut f, &press(KeyCode::Char('j')));
527 assert_eq!(f.focus, F::Header);
528 key(&mut f, &press(KeyCode::Char('k')));
529 assert_eq!(f.focus, F::Format);
530 assert_eq!(key(&mut f, &press(KeyCode::Char('x'))), FormKey::Other);
531 }
532
533 #[test]
534 fn the_picker_narrows_moves_and_never_types_a_space() {
535 let mut p = PickerState::new(vec!["alpha".into(), "beta".into(), "gamma".into()]);
536 assert_eq!(
537 picker_key(&mut p, false, &press(KeyCode::Down)),
538 PickerKey::Handled
539 );
540 assert_eq!(p.selected_original(), Some(1));
541 assert_eq!(
542 picker_key(&mut p, false, &press(KeyCode::Char('g'))),
543 PickerKey::Handled
544 );
545 assert_eq!(p.filter, "g");
546 assert_eq!(
547 picker_key(&mut p, false, &press(KeyCode::Char(' '))),
548 PickerKey::Choose
549 );
550 assert_eq!(
551 picker_key(&mut p, true, &press(KeyCode::Char(' '))),
552 PickerKey::Toggle
553 );
554 assert_eq!(p.filter, "g", "a space never narrows");
555 assert_eq!(
556 picker_key(&mut p, false, &press(KeyCode::Enter)),
557 PickerKey::Choose
558 );
559 assert_eq!(
560 picker_key(&mut p, false, &press(KeyCode::Esc)),
561 PickerKey::Close
562 );
563 assert_eq!(
564 picker_key(&mut p, false, &press(KeyCode::Tab)),
565 PickerKey::ChooseAndMove(true)
566 );
567 assert_eq!(
568 picker_key(&mut p, false, &press(KeyCode::BackTab)),
569 PickerKey::ChooseAndMove(false)
570 );
571 }
572
573 #[test]
574 fn steps_wrap_both_ways() {
575 assert_eq!(step_index(0, 3, -1), 2);
576 assert_eq!(step_index(2, 3, 1), 0);
577 assert_eq!(step_value(&['a', 'b', 'c'], 'b', 1), 'c');
578 assert_eq!(step_value(&['a', 'b', 'c'], 'z', 1), 'a');
579 }
580}