Skip to main content

snip_it/ui/
variables.rs

1//! Variable prompting dialog for snippet parameters.
2//!
3//! Displays a modal TUI dialog that prompts the user to fill in
4//! variable values for snippets using `<name=default>` syntax.
5//!
6//! Modal editing mirrors the main TUI:
7//!
8//! - **Insert (INS)** (default): typed characters are inserted at the cursor.
9//!   Pressing `q` simply inserts the letter `q` (or whatever char happens to
10//!   share its keybinding in NOR mode) so common words like "queue" type
11//!   naturally.
12//! - **Normal (NOR)**: navigation/editing commands (h/l for cursor,
13//!   j/k for variables, d for defaults, q to go back to the snippet
14//!   selector, i to return to INS).
15//!
16//! Only `Ctrl+C` (or SIGINT/SIGTERM) exits the whole program.
17//!
18//! The cursor is tracked per-field as a byte index on a Unicode scalar
19//! value boundary.
20
21use std::io;
22use std::time::Duration;
23
24use crossterm::event::{self, Event as CEvent, KeyCode, KeyEventKind, KeyModifiers};
25use ratatui::text::{Line, Span};
26use ratatui::{
27    layout::{Constraint, Direction, Layout},
28    style::Style as TuiStyle,
29    widgets::{Block, Borders, Clear, Paragraph},
30};
31
32use crate::utils::variables::Variable;
33
34use super::get_terminate;
35use super::state::is_ctrl_key;
36use super::theme::{get_theme, style_fg};
37
38/// RAII guard that restores the terminal when dropped.
39/// Ensures the terminal is always restored even on early return or panic.
40struct TerminalGuard;
41
42impl Drop for TerminalGuard {
43    fn drop(&mut self) {
44        ratatui::restore();
45    }
46}
47
48/// Result of prompting the user for variable values.
49pub enum VariablePromptResult {
50    /// User explicitly cancelled the program (`Ctrl+C` or SIGINT/SIGTERM).
51    /// The caller should exit the program entirely.
52    Cancel,
53    /// User backed out of the prompt (`q`). The caller should return to
54    /// the snippet selector without running the snippet.
55    Back,
56    /// No variables to fill (skipped the prompt entirely).
57    Skip,
58    /// User provided variable values.
59    Values(Vec<(String, String)>),
60}
61
62/// What a single keypress in the variable prompt should do.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64enum KeyAction {
65    /// Keep editing; the prompt stays open.
66    Continue,
67    /// Close the prompt and return to the snippet selector.
68    Back,
69}
70
71pub fn prompt_variables(vars: Vec<Variable>) -> io::Result<VariablePromptResult> {
72    if vars.is_empty() {
73        return Ok(VariablePromptResult::Skip);
74    }
75
76    prompt_variables_inner(vars)
77}
78
79/// Editable field for a single variable value.
80#[derive(Clone)]
81struct Field {
82    value: String,
83    /// Cursor position as a byte index on a char boundary in `value`.
84    cursor: usize,
85}
86
87impl Field {
88    fn new(default: &str) -> Self {
89        let value = default.to_string();
90        let cursor = value.len();
91        Self { value, cursor }
92    }
93
94    fn insert_char(&mut self, c: char) {
95        let pos = self.cursor.min(self.value.len());
96        self.value.insert(pos, c);
97        self.cursor = pos + c.len_utf8();
98    }
99
100    /// Backspace: remove the character before the cursor.
101    fn backspace(&mut self) {
102        if self.cursor == 0 {
103            return;
104        }
105        let prev = prev_char_boundary(&self.value, self.cursor);
106        self.value.drain(prev..self.cursor);
107        self.cursor = prev;
108    }
109
110    /// Delete: remove the character at the cursor.
111    fn delete(&mut self) {
112        if self.cursor >= self.value.len() {
113            return;
114        }
115        let next = next_char_boundary(&self.value, self.cursor);
116        self.value.drain(self.cursor..next);
117    }
118
119    fn move_left(&mut self) {
120        if self.cursor == 0 {
121            return;
122        }
123        self.cursor = prev_char_boundary(&self.value, self.cursor);
124    }
125
126    fn move_right(&mut self) {
127        if self.cursor >= self.value.len() {
128            return;
129        }
130        self.cursor = next_char_boundary(&self.value, self.cursor);
131    }
132
133    fn move_start(&mut self) {
134        self.cursor = 0;
135    }
136
137    fn move_end(&mut self) {
138        self.cursor = self.value.len();
139    }
140
141    fn clear(&mut self) {
142        self.value.clear();
143        self.cursor = 0;
144    }
145}
146
147/// Returns the byte index of the char boundary strictly before `idx` in `s`.
148/// `idx` is assumed to be a char boundary (`> 0` and `<= s.len()`).
149fn prev_char_boundary(s: &str, idx: usize) -> usize {
150    debug_assert!(idx <= s.len());
151    let mut i = idx - 1;
152    while !s.is_char_boundary(i) {
153        i -= 1;
154    }
155    i
156}
157
158/// Returns the byte index of the char boundary strictly after `idx` in `s`.
159/// `idx` is assumed to be a char boundary (`< s.len()`).
160fn next_char_boundary(s: &str, idx: usize) -> usize {
161    debug_assert!(idx < s.len());
162    let mut i = idx + 1;
163    while i < s.len() && !s.is_char_boundary(i) {
164        i += 1;
165    }
166    i
167}
168
169fn prompt_variables_inner(vars: Vec<Variable>) -> io::Result<VariablePromptResult> {
170    let mut terminal = ratatui::init();
171    let _guard = TerminalGuard; // Ensures terminal is restored on any exit path
172
173    let defaults: Vec<String> = vars
174        .iter()
175        .map(|v| v.default.clone().unwrap_or_default())
176        .collect();
177
178    let mut fields: Vec<Field> = defaults.iter().map(|d| Field::new(d)).collect();
179    let mut selected = 0usize;
180    let mut show_defaults = true;
181    let mut insert_mode = true;
182    let terminate = get_terminate();
183
184    loop {
185        if terminate.load(std::sync::atomic::Ordering::SeqCst) {
186            return Ok(VariablePromptResult::Cancel);
187        }
188
189        terminal.draw(|f| {
190            let size = f.area();
191            f.render_widget(Clear, size);
192            if size.width < 10 || size.height < 10 {
193                let error_msg = "Terminal too small - resize to at least 10x10";
194                let paragraph = Paragraph::new(error_msg)
195                    .centered()
196                    .block(Block::default().title("Error").borders(Borders::ALL));
197                f.render_widget(paragraph, size);
198                return;
199            }
200            let theme = get_theme();
201            let block = Block::default()
202                .title("Enter variables")
203                .borders(Borders::ALL)
204                .style(style_fg(theme.border));
205
206            let num_vars = fields.len();
207            let var_height = num_vars * 3;
208            let chunks = Layout::default()
209                .direction(Direction::Vertical)
210                .margin(2)
211                .constraints([
212                    Constraint::Length(var_height.min(u16::MAX as usize) as u16),
213                    Constraint::Length(1),
214                ])
215                .split(size);
216
217            f.render_widget(block, size);
218
219            let var_chunks = Layout::default()
220                .direction(Direction::Vertical)
221                .constraints(vec![Constraint::Length(3); num_vars])
222                .split(chunks[0]);
223
224            for (i, var) in vars.iter().enumerate() {
225                let var_block = Block::default()
226                    .title(var.name.as_str())
227                    .borders(Borders::ALL)
228                    .style(if i == selected {
229                        TuiStyle::default().fg(theme.accent)
230                    } else {
231                        TuiStyle::default()
232                    });
233
234                let prefix = if i == selected { "▶ " } else { "  " };
235                let display_value = if show_defaults
236                    && fields[i].value == defaults[i]
237                    && !defaults[i].is_empty()
238                {
239                    format!("{} (default: {})", fields[i].value, defaults[i])
240                } else if fields[i].value.is_empty() {
241                    "_".to_string()
242                } else {
243                    fields[i].value.clone()
244                };
245                let text = format!("{prefix}{display_value}");
246
247                let p = Paragraph::new(text)
248                    .block(var_block)
249                    .style(style_fg(theme.text));
250
251                f.render_widget(p, var_chunks[i]);
252            }
253
254            // Position the cursor inside the selected field at the tracked
255            // byte offset.
256            if selected < fields.len() {
257                use unicode_width::UnicodeWidthStr;
258                let prefix_len = 2;
259                let field = &fields[selected];
260                let cursor_byte = field.cursor.min(field.value.len());
261                let prefix_str = field.value.get(..cursor_byte).unwrap_or("");
262                let cursor_x = var_chunks[selected].x
263                    + 1
264                    + prefix_len
265                    + prefix_str.width().min(u16::MAX as usize) as u16;
266                let cursor_y = var_chunks[selected].y + 1;
267                f.set_cursor_position((cursor_x, cursor_y));
268            }
269
270            let status_text = if insert_mode {
271                "[INS] type to enter | \u{2190}/\u{2192}: move | \u{2191}/\u{2193}/Tab/ctrl+d/ctrl+u: var | Enter: save | Esc: normal"
272            } else {
273                "[NOR] i: insert | h/l: move | 0/$: start/end | j/k/Tab: var | x: delete | d: defaults | Enter: save | q: back | ctrl+c: quit"
274            };
275            let status_widget = Paragraph::new(Line::from(vec![Span::styled(
276                status_text,
277                style_fg(theme.muted),
278            )]));
279            f.render_widget(status_widget, chunks[1]);
280        })?;
281
282        let polled = event::poll(Duration::from_millis(200)).unwrap_or(false);
283        if polled {
284            let key_event = event::read().ok();
285            if let Some(CEvent::Key(key)) = key_event
286                && key.kind == KeyEventKind::Press
287            {
288                if is_ctrl_key(&key, 'c') {
289                    terminate.store(true, std::sync::atomic::Ordering::SeqCst);
290                    return Ok(VariablePromptResult::Cancel);
291                }
292                let action = if insert_mode {
293                    handle_insert_key(key, &mut fields, &mut selected, &defaults, &mut insert_mode)?
294                } else {
295                    handle_normal_key(
296                        key,
297                        &mut fields,
298                        &mut selected,
299                        &mut show_defaults,
300                        &mut insert_mode,
301                    )?
302                };
303                match action {
304                    KeyAction::Back => return Ok(VariablePromptResult::Back),
305                    KeyAction::Continue => {
306                        if matches!(key.code, KeyCode::Enter) {
307                            break;
308                        }
309                    }
310                }
311            }
312        }
313    }
314
315    for (field, default) in fields.iter_mut().zip(defaults.iter()) {
316        if field.value.is_empty() && !default.is_empty() {
317            field.value = default.clone();
318        }
319    }
320
321    let result: Vec<(String, String)> = vars
322        .iter()
323        .zip(fields.iter())
324        .map(|(v, f)| (v.name.clone(), f.value.trim().to_string()))
325        .collect();
326    Ok(VariablePromptResult::Values(result))
327}
328
329fn handle_insert_key(
330    key: crossterm::event::KeyEvent,
331    fields: &mut [Field],
332    selected: &mut usize,
333    defaults: &[String],
334    insert_mode: &mut bool,
335) -> io::Result<KeyAction> {
336    let num_vars = fields.len();
337    match key.code {
338        KeyCode::Esc => {
339            *insert_mode = false;
340        }
341        KeyCode::Up => {
342            if *selected > 0 {
343                *selected -= 1;
344            }
345        }
346        KeyCode::Down => {
347            if *selected + 1 < num_vars {
348                *selected += 1;
349            }
350        }
351        KeyCode::Tab => {
352            if *selected + 1 < num_vars {
353                *selected += 1;
354            } else {
355                *selected = 0;
356            }
357        }
358        KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
359            if *selected + 1 < num_vars {
360                *selected += 1;
361            }
362        }
363        KeyCode::Char('u') if key.modifiers.contains(KeyModifiers::CONTROL) => {
364            if *selected > 0 {
365                *selected -= 1;
366            }
367        }
368        KeyCode::Left => {
369            fields[*selected].move_left();
370        }
371        KeyCode::Right => {
372            fields[*selected].move_right();
373        }
374        KeyCode::Backspace => {
375            fields[*selected].backspace();
376        }
377        KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
378            let field = &mut fields[*selected];
379            if field.cursor >= field.value.len()
380                && !defaults[*selected].is_empty()
381                && field.value == defaults[*selected]
382            {
383                field.clear();
384            }
385            field.insert_char(c);
386        }
387        KeyCode::Enter => {}
388        _ => {}
389    }
390    Ok(KeyAction::Continue)
391}
392
393fn handle_normal_key(
394    key: crossterm::event::KeyEvent,
395    fields: &mut [Field],
396    selected: &mut usize,
397    show_defaults: &mut bool,
398    insert_mode: &mut bool,
399) -> io::Result<KeyAction> {
400    let num_vars = fields.len();
401    let mut action = KeyAction::Continue;
402    match key.code {
403        KeyCode::Char('q') => action = KeyAction::Back,
404        KeyCode::Char('i') | KeyCode::Esc => {
405            *insert_mode = true;
406        }
407        KeyCode::Char('a') => {
408            fields[*selected].move_right();
409            *insert_mode = true;
410        }
411        KeyCode::Char('A') => {
412            fields[*selected].move_end();
413            *insert_mode = true;
414        }
415        KeyCode::Char('I') => {
416            fields[*selected].move_start();
417            *insert_mode = true;
418        }
419        KeyCode::Char('h') | KeyCode::Left => {
420            fields[*selected].move_left();
421        }
422        KeyCode::Char('l') | KeyCode::Right => {
423            fields[*selected].move_right();
424        }
425        KeyCode::Char('0') => {
426            fields[*selected].move_start();
427        }
428        KeyCode::Char('$') => {
429            fields[*selected].move_end();
430        }
431        KeyCode::Char('x') | KeyCode::Delete => {
432            fields[*selected].delete();
433        }
434        KeyCode::Backspace => {
435            fields[*selected].backspace();
436        }
437        KeyCode::Char('j') | KeyCode::Down => {
438            if *selected + 1 < num_vars {
439                *selected += 1;
440            }
441        }
442        KeyCode::Char('k') | KeyCode::Up => {
443            if *selected > 0 {
444                *selected -= 1;
445            }
446        }
447        KeyCode::Tab => {
448            if *selected + 1 < num_vars {
449                *selected += 1;
450            } else {
451                *selected = 0;
452            }
453        }
454        KeyCode::Char('d') => {
455            *show_defaults = !*show_defaults;
456        }
457        KeyCode::Enter => {}
458        _ => {}
459    }
460    Ok(action)
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466
467    #[test]
468    fn field_new_places_cursor_at_end() {
469        let f = Field::new("hello");
470        assert_eq!(f.value, "hello");
471        assert_eq!(f.cursor, "hello".len());
472    }
473
474    #[test]
475    fn field_insert_at_cursor() {
476        let mut f = Field::new("hello");
477        f.cursor = 0;
478        f.insert_char('X');
479        assert_eq!(f.value, "Xhello");
480        assert_eq!(f.cursor, 1);
481    }
482
483    #[test]
484    fn field_insert_in_middle() {
485        let mut f = Field::new("hello");
486        f.cursor = 2;
487        f.insert_char('X');
488        assert_eq!(f.value, "heXllo");
489        assert_eq!(f.cursor, 3);
490    }
491
492    #[test]
493    fn field_backspace_removes_previous_char() {
494        let mut f = Field::new("hello");
495        f.backspace();
496        assert_eq!(f.value, "hell");
497        assert_eq!(f.cursor, 4);
498    }
499
500    #[test]
501    fn field_backspace_at_start_is_noop() {
502        let mut f = Field::new("hello");
503        f.cursor = 0;
504        f.backspace();
505        assert_eq!(f.value, "hello");
506        assert_eq!(f.cursor, 0);
507    }
508
509    #[test]
510    fn field_delete_removes_char_at_cursor() {
511        let mut f = Field::new("hello");
512        f.cursor = 1;
513        f.delete();
514        assert_eq!(f.value, "hllo");
515        assert_eq!(f.cursor, 1);
516    }
517
518    #[test]
519    fn field_move_left_right() {
520        let mut f = Field::new("hello");
521        f.move_left();
522        f.move_left();
523        assert_eq!(f.cursor, 3);
524        f.move_right();
525        assert_eq!(f.cursor, 4);
526    }
527
528    #[test]
529    fn field_handles_multibyte_chars() {
530        let mut f = Field::new("héllo");
531        assert_eq!(f.value.len(), 6);
532        f.cursor = 1;
533        f.insert_char('X');
534        assert_eq!(f.value, "hXéllo");
535        assert_eq!(f.cursor, 2);
536        f.backspace();
537        assert_eq!(f.value, "héllo");
538        assert_eq!(f.cursor, 1);
539    }
540
541    #[test]
542    fn field_start_end() {
543        let mut f = Field::new("hello");
544        f.move_start();
545        assert_eq!(f.cursor, 0);
546        f.move_end();
547        assert_eq!(f.cursor, 5);
548    }
549
550    #[test]
551    fn field_clear() {
552        let mut f = Field::new("hello");
553        f.clear();
554        assert_eq!(f.value, "");
555        assert_eq!(f.cursor, 0);
556    }
557
558    #[test]
559    fn prompt_variables_with_no_vars_skips() {
560        let result = prompt_variables(vec![]).unwrap();
561        assert!(matches!(result, VariablePromptResult::Skip));
562    }
563
564    fn press(c: char) -> crossterm::event::KeyEvent {
565        crossterm::event::KeyEvent {
566            code: crossterm::event::KeyCode::Char(c),
567            modifiers: crossterm::event::KeyModifiers::empty(),
568            kind: crossterm::event::KeyEventKind::Press,
569            state: crossterm::event::KeyEventState::NONE,
570        }
571    }
572
573    fn press_ctrl(c: char) -> crossterm::event::KeyEvent {
574        crossterm::event::KeyEvent {
575            code: crossterm::event::KeyCode::Char(c),
576            modifiers: crossterm::event::KeyModifiers::CONTROL,
577            kind: crossterm::event::KeyEventKind::Press,
578            state: crossterm::event::KeyEventState::NONE,
579        }
580    }
581
582    #[test]
583    fn ins_mode_inserts_alphanumerics_including_q() {
584        let mut fields = vec![Field::new("")];
585        let mut selected = 0;
586        let defaults = vec![String::new()];
587        let mut insert_mode = true;
588
589        for c in "djckDJCKqQ".chars() {
590            let action = handle_insert_key(
591                press(c),
592                &mut fields,
593                &mut selected,
594                &defaults,
595                &mut insert_mode,
596            )
597            .unwrap();
598            assert_eq!(action, KeyAction::Continue, "{c:?} should continue");
599            assert!(insert_mode, "{c:?} should keep INS mode");
600        }
601        assert_eq!(fields[0].value, "djckDJCKqQ");
602    }
603
604    #[test]
605    fn ins_mode_q_inserts_as_char() {
606        // 'q' is reserved as "back to selector" in NOR mode, but in INS mode
607        // it must type as a regular character so words like "queue" or "q1"
608        // can be entered.
609        let mut fields = vec![Field::new("")];
610        let mut selected = 0;
611        let defaults = vec![String::new()];
612        let mut insert_mode = true;
613
614        for c in "queue".chars() {
615            let action = handle_insert_key(
616                press(c),
617                &mut fields,
618                &mut selected,
619                &defaults,
620                &mut insert_mode,
621            )
622            .unwrap();
623            assert_eq!(action, KeyAction::Continue);
624        }
625        assert_eq!(fields[0].value, "queue");
626    }
627
628    #[test]
629    fn ins_mode_ctrl_d_moves_to_next_var() {
630        let mut fields = vec![Field::new("a"), Field::new("b"), Field::new("c")];
631        let mut selected = 0;
632        let defaults = vec!["a".to_string(), "b".to_string(), "c".to_string()];
633        let mut insert_mode = true;
634
635        let action = handle_insert_key(
636            press_ctrl('d'),
637            &mut fields,
638            &mut selected,
639            &defaults,
640            &mut insert_mode,
641        )
642        .unwrap();
643        assert_eq!(action, KeyAction::Continue);
644        assert_eq!(selected, 1);
645        assert!(insert_mode);
646    }
647
648    #[test]
649    fn ins_mode_ctrl_d_at_last_var_clamps() {
650        let mut fields = vec![Field::new("a"), Field::new("b")];
651        let mut selected = 1;
652        let defaults = vec!["a".to_string(), "b".to_string()];
653        let mut insert_mode = true;
654
655        let action = handle_insert_key(
656            press_ctrl('d'),
657            &mut fields,
658            &mut selected,
659            &defaults,
660            &mut insert_mode,
661        )
662        .unwrap();
663        assert_eq!(action, KeyAction::Continue);
664        assert_eq!(selected, 1, "should stay at last var");
665    }
666
667    #[test]
668    fn ins_mode_ctrl_u_moves_to_prev_var() {
669        let mut fields = vec![Field::new("a"), Field::new("b"), Field::new("c")];
670        let mut selected = 2;
671        let defaults = vec!["a".to_string(), "b".to_string(), "c".to_string()];
672        let mut insert_mode = true;
673
674        let action = handle_insert_key(
675            press_ctrl('u'),
676            &mut fields,
677            &mut selected,
678            &defaults,
679            &mut insert_mode,
680        )
681        .unwrap();
682        assert_eq!(action, KeyAction::Continue);
683        assert_eq!(selected, 1);
684        assert!(insert_mode);
685    }
686
687    #[test]
688    fn ins_mode_ctrl_u_at_first_var_clamps() {
689        let mut fields = vec![Field::new("a"), Field::new("b")];
690        let mut selected = 0;
691        let defaults = vec!["a".to_string(), "b".to_string()];
692        let mut insert_mode = true;
693
694        let action = handle_insert_key(
695            press_ctrl('u'),
696            &mut fields,
697            &mut selected,
698            &defaults,
699            &mut insert_mode,
700        )
701        .unwrap();
702        assert_eq!(action, KeyAction::Continue);
703        assert_eq!(selected, 0, "should stay at first var");
704    }
705
706    #[test]
707    fn ins_mode_plain_d_still_inserts() {
708        let mut fields = vec![Field::new("")];
709        let mut selected = 0;
710        let defaults = vec![String::new()];
711        let mut insert_mode = true;
712
713        let action = handle_insert_key(
714            press('d'),
715            &mut fields,
716            &mut selected,
717            &defaults,
718            &mut insert_mode,
719        )
720        .unwrap();
721        assert_eq!(action, KeyAction::Continue);
722        assert_eq!(fields[0].value, "d");
723    }
724
725    #[test]
726    fn ins_mode_plain_u_still_inserts() {
727        let mut fields = vec![Field::new("")];
728        let mut selected = 0;
729        let defaults = vec![String::new()];
730        let mut insert_mode = true;
731
732        let action = handle_insert_key(
733            press('u'),
734            &mut fields,
735            &mut selected,
736            &defaults,
737            &mut insert_mode,
738        )
739        .unwrap();
740        assert_eq!(action, KeyAction::Continue);
741        assert_eq!(fields[0].value, "u");
742    }
743
744    #[test]
745    fn ins_mode_typing_into_default_clears_then_inserts() {
746        let mut fields = vec![Field::new("any.com")];
747        let mut selected = 0;
748        let defaults = vec!["any.com".to_string()];
749        let mut insert_mode = true;
750
751        for c in "dandy.com".chars() {
752            let action = handle_insert_key(
753                press(c),
754                &mut fields,
755                &mut selected,
756                &defaults,
757                &mut insert_mode,
758            )
759            .unwrap();
760            assert_eq!(action, KeyAction::Continue);
761        }
762        assert_eq!(fields[0].value, "dandy.com");
763    }
764
765    #[test]
766    fn nor_mode_q_returns_back_action() {
767        let mut fields = vec![Field::new("hello")];
768        let mut selected = 0;
769        let mut show_defaults = true;
770        let mut insert_mode = false;
771
772        let action = handle_normal_key(
773            press('q'),
774            &mut fields,
775            &mut selected,
776            &mut show_defaults,
777            &mut insert_mode,
778        )
779        .unwrap();
780        assert_eq!(action, KeyAction::Back);
781        assert_eq!(fields[0].value, "hello");
782    }
783
784    #[test]
785    fn ins_mode_ctrl_c_is_not_treated_as_char() {
786        // The dispatcher checks Ctrl+C before the handler, but verify the
787        // INS handler itself would not blindly insert a control-modified
788        // character: with the CONTROL guard, Char(c) is only matched when
789        // the modifier is absent.
790        let mut fields = vec![Field::new("")];
791        let mut selected = 0;
792        let defaults = vec![String::new()];
793        let mut insert_mode = true;
794
795        let action = handle_insert_key(
796            press_ctrl('c'),
797            &mut fields,
798            &mut selected,
799            &defaults,
800            &mut insert_mode,
801        )
802        .unwrap();
803        assert_eq!(action, KeyAction::Continue);
804        assert_eq!(fields[0].value, "", "Ctrl+C must not insert 'c'");
805    }
806}