Skip to main content

huh/
lib.rs

1#![forbid(unsafe_code)]
2// Allow pedantic lints for early-stage API ergonomics.
3#![allow(clippy::nursery)]
4#![allow(clippy::pedantic)]
5
6//! # Huh
7//!
8//! A library for building interactive forms and prompts in the terminal.
9//!
10//! Huh provides a declarative way to create:
11//! - Text inputs and text areas
12//! - Select menus and multi-select
13//! - Confirmations and notes
14//! - Grouped form fields
15//! - Accessible, keyboard-navigable interfaces
16//!
17//! ## Role in `charmed_rust`
18//!
19//! Huh is the form and prompt layer built on bubbletea and bubbles:
20//! - **bubbletea** provides the runtime and update loop.
21//! - **bubbles** supplies reusable widgets (text input, list, etc.).
22//! - **lipgloss** handles consistent styling and themes.
23//! - **demo_showcase** uses huh to demonstrate multi-step workflows.
24//!
25//! ## Example
26//!
27//! ```rust,ignore
28//! use huh::{Form, Group, Input, Select, SelectOption, Confirm};
29//! use bubbletea::Program;
30//!
31//! let form = Form::new(vec![
32//!     Group::new(vec![
33//!         Box::new(Input::new()
34//!             .key("name")
35//!             .title("What's your name?")),
36//!         Box::new(Select::new()
37//!             .key("color")
38//!             .title("Favorite color?")
39//!             .options(vec![
40//!                 SelectOption::new("Red", "red"),
41//!                 SelectOption::new("Green", "green"),
42//!                 SelectOption::new("Blue", "blue"),
43//!             ])),
44//!     ]),
45//!     Group::new(vec![
46//!         Box::new(Confirm::new()
47//!             .key("confirm")
48//!             .title("Are you sure?")),
49//!     ]),
50//! ]);
51//!
52//! let form = Program::new(form).run()?;
53//!
54//! let name = form.get_string("name").unwrap();
55//! let color = form.get_string("color").unwrap();
56//! let confirm = form.get_bool("confirm").unwrap();
57//!
58//! println!("Name: {}, Color: {}, Confirmed: {}", name, color, confirm);
59//! ```
60
61use std::any::Any;
62use std::sync::atomic::{AtomicUsize, Ordering};
63
64use thiserror::Error;
65
66use bubbles::key::Binding;
67use bubbletea::{Cmd, KeyMsg, KeyType, Message, Model};
68use lipgloss::{Border, Style};
69
70// -----------------------------------------------------------------------------
71// ID Generation
72// -----------------------------------------------------------------------------
73
74static LAST_ID: AtomicUsize = AtomicUsize::new(0);
75
76fn next_id() -> usize {
77    LAST_ID.fetch_add(1, Ordering::SeqCst)
78}
79
80// -----------------------------------------------------------------------------
81// Errors
82// -----------------------------------------------------------------------------
83
84/// Errors that can occur during form execution.
85///
86/// This enum represents all possible error conditions when running
87/// an interactive form with huh.
88///
89/// # Error Handling
90///
91/// Forms can fail for several reasons, but many are recoverable
92/// or expected user actions (like cancellation):
93///
94/// ```rust,ignore
95/// use huh::{Form, FormError, Result};
96///
97/// fn get_user_input() -> Result<String> {
98///     let mut name = String::new();
99///     Form::new(fields)
100///         .run()?;
101///     Ok(name)
102/// }
103/// ```
104///
105/// # Recovery Strategies
106///
107/// | Error Variant | Recovery Strategy |
108/// |--------------|-------------------|
109/// | [`UserAborted`](FormError::UserAborted) | Normal exit, not an error condition |
110/// | [`Timeout`](FormError::Timeout) | Retry with longer timeout or prompt user |
111/// | [`Validation`](FormError::Validation) | Show error message, allow retry |
112/// | [`Io`](FormError::Io) | Check terminal, fall back to non-interactive |
113///
114/// # Example: Handling User Abort
115///
116/// User abort (Ctrl+C) is a normal exit path, not an error:
117///
118/// ```rust,ignore
119/// match form.run() {
120///     Ok(()) => println!("Form completed!"),
121///     Err(FormError::UserAborted) => {
122///         println!("Cancelled by user");
123///         return Ok(()); // Not an error condition
124///     }
125///     Err(e) => return Err(e.into()),
126/// }
127/// ```
128///
129/// # Note on Clone and PartialEq
130///
131/// This error type implements `Clone` and `PartialEq` to support
132/// testing and comparison. As a result, the `Io` variant stores
133/// a `String` message rather than the underlying `io::Error`.
134#[derive(Error, Debug, Clone, PartialEq, Eq)]
135pub enum FormError {
136    /// User aborted the form with Ctrl+C or Escape.
137    ///
138    /// This is not an error condition but a normal exit path.
139    /// Users may cancel forms for valid reasons, and applications
140    /// should handle this gracefully.
141    ///
142    /// # Example
143    ///
144    /// ```rust,ignore
145    /// match form.run() {
146    ///     Err(FormError::UserAborted) => {
147    ///         println!("No changes made");
148    ///         return Ok(());
149    ///     }
150    ///     // ...
151    /// }
152    /// ```
153    #[error("user aborted")]
154    UserAborted,
155
156    /// Form execution timed out.
157    ///
158    /// Occurs when a form has a timeout configured and the user
159    /// does not complete it in time.
160    ///
161    /// # Recovery
162    ///
163    /// - Increase the timeout duration
164    /// - Prompt user to try again
165    /// - Use a default value
166    #[error("timeout")]
167    Timeout,
168
169    /// Custom validation error.
170    ///
171    /// Occurs when a field's validation function returns an error.
172    /// The contained string describes what validation failed.
173    ///
174    /// # Recovery
175    ///
176    /// Validation errors are recoverable - show the error message
177    /// to the user and allow them to correct their input.
178    ///
179    /// # Example
180    ///
181    /// ```rust,ignore
182    /// let input = Input::new()
183    ///     .title("Email")
184    ///     .validate(|s| {
185    ///         if s.contains('@') {
186    ///             Ok(())
187    ///         } else {
188    ///             Err(FormError::Validation("must contain @".into()))
189    ///         }
190    ///     });
191    /// ```
192    #[error("validation error: {0}")]
193    Validation(String),
194
195    /// IO error during form operations.
196    ///
197    /// Occurs during terminal I/O operations, particularly in
198    /// accessible mode where stdin/stdout are used directly.
199    ///
200    /// Note: Stores the error message as a `String` rather than
201    /// `io::Error` to maintain `Clone` and `PartialEq` derives.
202    ///
203    /// # Recovery
204    ///
205    /// - Check if the terminal is available
206    /// - Fall back to non-interactive input
207    /// - Log the error and exit gracefully
208    #[error("io error: {0}")]
209    Io(String),
210}
211
212impl FormError {
213    /// Creates a validation error with the given message.
214    pub fn validation(message: impl Into<String>) -> Self {
215        Self::Validation(message.into())
216    }
217
218    /// Creates an IO error with the given message.
219    pub fn io(message: impl Into<String>) -> Self {
220        Self::Io(message.into())
221    }
222
223    /// Returns true if this is a user-initiated abort.
224    pub fn is_user_abort(&self) -> bool {
225        matches!(self, Self::UserAborted)
226    }
227
228    /// Returns true if this is a timeout error.
229    pub fn is_timeout(&self) -> bool {
230        matches!(self, Self::Timeout)
231    }
232
233    /// Returns true if this error is recoverable (validation errors).
234    pub fn is_recoverable(&self) -> bool {
235        matches!(self, Self::Validation(_))
236    }
237}
238
239/// A specialized [`Result`] type for huh form operations.
240///
241/// This type alias defaults to [`FormError`] as the error type.
242///
243/// # Example
244///
245/// ```rust,ignore
246/// use huh::Result;
247///
248/// fn collect_user_info() -> Result<UserInfo> {
249///     let mut name = String::new();
250///     let mut email = String::new();
251///
252///     Form::new(vec![/* fields */]).run()?;
253///
254///     Ok(UserInfo { name, email })
255/// }
256/// ```
257pub type Result<T> = std::result::Result<T, FormError>;
258
259// -----------------------------------------------------------------------------
260// Form State
261// -----------------------------------------------------------------------------
262
263/// The current state of the form.
264#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
265pub enum FormState {
266    /// User is completing the form.
267    #[default]
268    Normal,
269    /// User has completed the form.
270    Completed,
271    /// User has aborted the form.
272    Aborted,
273}
274
275// -----------------------------------------------------------------------------
276// SelectOption
277// -----------------------------------------------------------------------------
278
279/// An option for select fields.
280#[derive(Debug, Clone, PartialEq, Eq)]
281pub struct SelectOption<T: Clone + PartialEq> {
282    /// The display key shown to the user.
283    pub key: String,
284    /// The underlying value.
285    pub value: T,
286    /// Whether this option is initially selected.
287    pub selected: bool,
288}
289
290impl<T: Clone + PartialEq> SelectOption<T> {
291    /// Creates a new option.
292    pub fn new(key: impl Into<String>, value: T) -> Self {
293        Self {
294            key: key.into(),
295            value,
296            selected: false,
297        }
298    }
299
300    /// Sets whether the option is initially selected.
301    pub fn selected(mut self, selected: bool) -> Self {
302        self.selected = selected;
303        self
304    }
305}
306
307impl<T: Clone + PartialEq + std::fmt::Display> SelectOption<T> {
308    /// Creates options from a list of values using Display for keys.
309    pub fn from_values(values: impl IntoIterator<Item = T>) -> Vec<Self> {
310        values
311            .into_iter()
312            .map(|v| Self::new(v.to_string(), v))
313            .collect()
314    }
315}
316
317/// Creates options from string values.
318pub fn new_options<S: Into<String> + Clone>(
319    values: impl IntoIterator<Item = S>,
320) -> Vec<SelectOption<String>> {
321    values
322        .into_iter()
323        .map(|v| {
324            let s: String = v.clone().into();
325            SelectOption::new(s.clone(), s)
326        })
327        .collect()
328}
329
330// -----------------------------------------------------------------------------
331// Theme
332// -----------------------------------------------------------------------------
333
334/// Collection of styles for form components.
335#[derive(Debug, Clone)]
336pub struct Theme {
337    /// Styles for the form container.
338    pub form: FormStyles,
339    /// Styles for groups.
340    pub group: GroupStyles,
341    /// Separator between fields.
342    pub field_separator: Style,
343    /// Styles for blurred (unfocused) fields.
344    pub blurred: FieldStyles,
345    /// Styles for focused fields.
346    pub focused: FieldStyles,
347    /// Style for help text at the bottom of the form.
348    pub help: Style,
349}
350
351impl Default for Theme {
352    fn default() -> Self {
353        theme_charm()
354    }
355}
356
357/// Styles for the form container.
358#[derive(Debug, Clone, Default)]
359pub struct FormStyles {
360    /// Base style for the form.
361    pub base: Style,
362}
363
364/// Styles for groups.
365#[derive(Debug, Clone, Default)]
366pub struct GroupStyles {
367    /// Base style for the group.
368    pub base: Style,
369    /// Title style.
370    pub title: Style,
371    /// Description style.
372    pub description: Style,
373}
374
375/// Styles for input fields.
376#[derive(Debug, Clone, Default)]
377pub struct FieldStyles {
378    /// Base style.
379    pub base: Style,
380    /// Title style.
381    pub title: Style,
382    /// Description style.
383    pub description: Style,
384    /// Error indicator style.
385    pub error_indicator: Style,
386    /// Error message style.
387    pub error_message: Style,
388
389    // Select styles
390    /// Select cursor style.
391    pub select_selector: Style,
392    /// Option style.
393    pub option: Style,
394    /// Next indicator for inline select.
395    pub next_indicator: Style,
396    /// Previous indicator for inline select.
397    pub prev_indicator: Style,
398
399    // Multi-select styles
400    /// Multi-select cursor style.
401    pub multi_select_selector: Style,
402    /// Selected option style.
403    pub selected_option: Style,
404    /// Selected prefix style.
405    pub selected_prefix: Style,
406    /// Unselected option style.
407    pub unselected_option: Style,
408    /// Unselected prefix style.
409    pub unselected_prefix: Style,
410
411    // Text input styles
412    /// Text input specific styles.
413    pub text_input: TextInputStyles,
414
415    // Confirm styles
416    /// Focused button style.
417    pub focused_button: Style,
418    /// Blurred button style.
419    pub blurred_button: Style,
420
421    // Note styles
422    /// Note title style.
423    pub note_title: Style,
424}
425
426/// Styles for text inputs.
427#[derive(Debug, Clone, Default)]
428pub struct TextInputStyles {
429    /// Cursor style.
430    pub cursor: Style,
431    /// Cursor text style.
432    pub cursor_text: Style,
433    /// Placeholder style.
434    pub placeholder: Style,
435    /// Prompt style.
436    pub prompt: Style,
437    /// Text style.
438    pub text: Style,
439}
440
441/// Returns the base theme.
442#[allow(clippy::field_reassign_with_default)]
443pub fn theme_base() -> Theme {
444    let button = Style::new().padding((0, 2)).margin_right(1);
445
446    let mut focused = FieldStyles::default();
447    focused.base = Style::new()
448        .padding_left(1)
449        .border(Border::thick())
450        .border_left(true);
451    focused.error_indicator = Style::new().set_string(" *");
452    focused.error_message = Style::new().set_string(" *");
453    focused.select_selector = Style::new().set_string("> ");
454    focused.next_indicator = Style::new().margin_left(1).set_string("→");
455    focused.prev_indicator = Style::new().margin_right(1).set_string("←");
456    focused.multi_select_selector = Style::new().set_string("> ");
457    focused.selected_prefix = Style::new().set_string("[•] ");
458    focused.unselected_prefix = Style::new().set_string("[ ] ");
459    focused.focused_button = button.clone().foreground("0").background("7");
460    focused.blurred_button = button.foreground("7").background("0");
461    focused.text_input.placeholder = Style::new().foreground("8");
462
463    let mut blurred = focused.clone();
464    blurred.base = blurred.base.border(Border::hidden());
465    blurred.multi_select_selector = Style::new().set_string("  ");
466    blurred.next_indicator = Style::new();
467    blurred.prev_indicator = Style::new();
468
469    Theme {
470        form: FormStyles { base: Style::new() },
471        group: GroupStyles::default(),
472        field_separator: Style::new().set_string("\n\n"),
473        focused,
474        blurred,
475        help: Style::new().foreground("241").margin_top(1),
476    }
477}
478
479/// Returns the Charm theme (default).
480pub fn theme_charm() -> Theme {
481    let mut t = theme_base();
482
483    let indigo = "#7571F9";
484    let fuchsia = "#F780E2";
485    let green = "#02BF87";
486    let red = "#ED567A";
487    let normal_fg = "252";
488
489    t.focused.base = t.focused.base.border_foreground("238");
490    t.focused.title = t.focused.title.foreground(indigo).bold();
491    t.focused.note_title = t
492        .focused
493        .note_title
494        .foreground(indigo)
495        .bold()
496        .margin_bottom(1);
497    t.focused.description = t.focused.description.foreground("243");
498    t.focused.error_indicator = t.focused.error_indicator.foreground(red);
499    t.focused.error_message = t.focused.error_message.foreground(red);
500    t.focused.select_selector = t.focused.select_selector.foreground(fuchsia);
501    t.focused.next_indicator = t.focused.next_indicator.foreground(fuchsia);
502    t.focused.prev_indicator = t.focused.prev_indicator.foreground(fuchsia);
503    t.focused.option = t.focused.option.foreground(normal_fg);
504    t.focused.multi_select_selector = t.focused.multi_select_selector.foreground(fuchsia);
505    t.focused.selected_option = t.focused.selected_option.foreground(green);
506    t.focused.selected_prefix = Style::new().foreground("#02A877").set_string("✓ ");
507    t.focused.unselected_prefix = Style::new().foreground("243").set_string("• ");
508    t.focused.unselected_option = t.focused.unselected_option.foreground(normal_fg);
509    t.focused.focused_button = t
510        .focused
511        .focused_button
512        .foreground("#FFFDF5")
513        .background(fuchsia);
514    t.focused.blurred_button = t
515        .focused
516        .blurred_button
517        .foreground(normal_fg)
518        .background("237");
519    t.focused.text_input.cursor = t.focused.text_input.cursor.foreground(green);
520    t.focused.text_input.placeholder = t.focused.text_input.placeholder.foreground("238");
521    t.focused.text_input.prompt = t.focused.text_input.prompt.foreground(fuchsia);
522
523    t.blurred = t.focused.clone();
524    t.blurred.base = t.focused.base.clone().border(Border::hidden());
525    t.blurred.next_indicator = Style::new();
526    t.blurred.prev_indicator = Style::new();
527
528    t.group.title = t.focused.title.clone();
529    t.group.description = t.focused.description.clone();
530    t.help = Style::new().foreground("241").margin_top(1);
531
532    t
533}
534
535/// Returns the Dracula theme.
536pub fn theme_dracula() -> Theme {
537    let mut t = theme_base();
538
539    let selection = "#44475a";
540    let foreground = "#f8f8f2";
541    let comment = "#6272a4";
542    let green = "#50fa7b";
543    let purple = "#bd93f9";
544    let red = "#ff5555";
545    let yellow = "#f1fa8c";
546
547    t.focused.base = t.focused.base.border_foreground(selection);
548    t.focused.title = t.focused.title.foreground(purple);
549    t.focused.note_title = t.focused.note_title.foreground(purple);
550    t.focused.description = t.focused.description.foreground(comment);
551    t.focused.error_indicator = t.focused.error_indicator.foreground(red);
552    t.focused.error_message = t.focused.error_message.foreground(red);
553    t.focused.select_selector = t.focused.select_selector.foreground(yellow);
554    t.focused.next_indicator = t.focused.next_indicator.foreground(yellow);
555    t.focused.prev_indicator = t.focused.prev_indicator.foreground(yellow);
556    t.focused.option = t.focused.option.foreground(foreground);
557    t.focused.multi_select_selector = t.focused.multi_select_selector.foreground(yellow);
558    t.focused.selected_option = t.focused.selected_option.foreground(green);
559    t.focused.selected_prefix = t.focused.selected_prefix.foreground(green);
560    t.focused.unselected_option = t.focused.unselected_option.foreground(foreground);
561    t.focused.unselected_prefix = t.focused.unselected_prefix.foreground(comment);
562    t.focused.focused_button = t
563        .focused
564        .focused_button
565        .foreground(yellow)
566        .background(purple)
567        .bold();
568    t.focused.blurred_button = t
569        .focused
570        .blurred_button
571        .foreground(foreground)
572        .background("#282a36");
573    t.focused.text_input.cursor = t.focused.text_input.cursor.foreground(yellow);
574    t.focused.text_input.placeholder = t.focused.text_input.placeholder.foreground(comment);
575    t.focused.text_input.prompt = t.focused.text_input.prompt.foreground(yellow);
576
577    t.blurred = t.focused.clone();
578    t.blurred.base = t.blurred.base.border(Border::hidden());
579    t.blurred.next_indicator = Style::new();
580    t.blurred.prev_indicator = Style::new();
581
582    t.group.title = t.focused.title.clone();
583    t.group.description = t.focused.description.clone();
584    t.help = Style::new().foreground(comment).margin_top(1);
585
586    t
587}
588
589/// Returns the Base16 theme.
590pub fn theme_base16() -> Theme {
591    let mut t = theme_base();
592
593    t.focused.base = t.focused.base.border_foreground("8");
594    t.focused.title = t.focused.title.foreground("6");
595    t.focused.note_title = t.focused.note_title.foreground("6");
596    t.focused.description = t.focused.description.foreground("8");
597    t.focused.error_indicator = t.focused.error_indicator.foreground("9");
598    t.focused.error_message = t.focused.error_message.foreground("9");
599    t.focused.select_selector = t.focused.select_selector.foreground("3");
600    t.focused.next_indicator = t.focused.next_indicator.foreground("3");
601    t.focused.prev_indicator = t.focused.prev_indicator.foreground("3");
602    t.focused.option = t.focused.option.foreground("7");
603    t.focused.multi_select_selector = t.focused.multi_select_selector.foreground("3");
604    t.focused.selected_option = t.focused.selected_option.foreground("2");
605    t.focused.selected_prefix = t.focused.selected_prefix.foreground("2");
606    t.focused.unselected_option = t.focused.unselected_option.foreground("7");
607    t.focused.focused_button = t.focused.focused_button.foreground("7").background("5");
608    t.focused.blurred_button = t.focused.blurred_button.foreground("7").background("0");
609
610    t.blurred = t.focused.clone();
611    t.blurred.base = t.blurred.base.border(Border::hidden());
612    t.blurred.note_title = t.blurred.note_title.foreground("8");
613    t.blurred.title = t.blurred.title.foreground("8");
614    t.blurred.text_input.prompt = t.blurred.text_input.prompt.foreground("8");
615    t.blurred.text_input.text = t.blurred.text_input.text.foreground("7");
616    t.blurred.next_indicator = Style::new();
617    t.blurred.prev_indicator = Style::new();
618
619    t.group.title = t.focused.title.clone();
620    t.group.description = t.focused.description.clone();
621    t.help = Style::new().foreground("8").margin_top(1);
622
623    t
624}
625
626/// Returns the Catppuccin theme.
627///
628/// This theme is based on the Catppuccin color scheme (Mocha variant).
629/// See <https://github.com/catppuccin/catppuccin> for more details.
630pub fn theme_catppuccin() -> Theme {
631    let mut t = theme_base();
632
633    // Catppuccin Mocha palette
634    let base = "#1e1e2e";
635    let text = "#cdd6f4";
636    let subtext1 = "#bac2de";
637    let subtext0 = "#a6adc8";
638    let _overlay1 = "#7f849c";
639    let overlay0 = "#6c7086";
640    let green = "#a6e3a1";
641    let red = "#f38ba8";
642    let pink = "#f5c2e7";
643    let mauve = "#cba6f7";
644    let rosewater = "#f5e0dc";
645
646    t.focused.base = t.focused.base.border_foreground(subtext1);
647    t.focused.title = t.focused.title.foreground(mauve);
648    t.focused.note_title = t.focused.note_title.foreground(mauve);
649    t.focused.description = t.focused.description.foreground(subtext0);
650    t.focused.error_indicator = t.focused.error_indicator.foreground(red);
651    t.focused.error_message = t.focused.error_message.foreground(red);
652    t.focused.select_selector = t.focused.select_selector.foreground(pink);
653    t.focused.next_indicator = t.focused.next_indicator.foreground(pink);
654    t.focused.prev_indicator = t.focused.prev_indicator.foreground(pink);
655    t.focused.option = t.focused.option.foreground(text);
656    t.focused.multi_select_selector = t.focused.multi_select_selector.foreground(pink);
657    t.focused.selected_option = t.focused.selected_option.foreground(green);
658    t.focused.selected_prefix = t.focused.selected_prefix.foreground(green);
659    t.focused.unselected_prefix = t.focused.unselected_prefix.foreground(text);
660    t.focused.unselected_option = t.focused.unselected_option.foreground(text);
661    t.focused.focused_button = t.focused.focused_button.foreground(base).background(pink);
662    t.focused.blurred_button = t.focused.blurred_button.foreground(text).background(base);
663
664    t.focused.text_input.cursor = t.focused.text_input.cursor.foreground(rosewater);
665    t.focused.text_input.placeholder = t.focused.text_input.placeholder.foreground(overlay0);
666    t.focused.text_input.prompt = t.focused.text_input.prompt.foreground(pink);
667
668    t.blurred = t.focused.clone();
669    t.blurred.base = t.blurred.base.border(Border::hidden());
670    t.blurred.next_indicator = Style::new();
671    t.blurred.prev_indicator = Style::new();
672
673    t.group.title = t.focused.title.clone();
674    t.group.description = t.focused.description.clone();
675    t.help = Style::new().foreground(subtext0).margin_top(1);
676
677    t
678}
679
680// -----------------------------------------------------------------------------
681// KeyMap
682// -----------------------------------------------------------------------------
683
684/// Keybindings for form navigation.
685#[derive(Debug, Clone)]
686pub struct KeyMap {
687    /// Quit the form.
688    pub quit: Binding,
689    /// Input field keybindings.
690    pub input: InputKeyMap,
691    /// Select field keybindings.
692    pub select: SelectKeyMap,
693    /// Multi-select field keybindings.
694    pub multi_select: MultiSelectKeyMap,
695    /// Confirm field keybindings.
696    pub confirm: ConfirmKeyMap,
697    /// Note field keybindings.
698    pub note: NoteKeyMap,
699    /// Text area keybindings.
700    pub text: TextKeyMap,
701    /// File picker keybindings.
702    pub file_picker: FilePickerKeyMap,
703}
704
705impl Default for KeyMap {
706    fn default() -> Self {
707        Self::new()
708    }
709}
710
711impl KeyMap {
712    /// Creates a new default keymap.
713    pub fn new() -> Self {
714        Self {
715            quit: Binding::new().keys(&["ctrl+c"]),
716            input: InputKeyMap::default(),
717            select: SelectKeyMap::default(),
718            multi_select: MultiSelectKeyMap::default(),
719            confirm: ConfirmKeyMap::default(),
720            note: NoteKeyMap::default(),
721            text: TextKeyMap::default(),
722            file_picker: FilePickerKeyMap::default(),
723        }
724    }
725}
726
727/// Keybindings for input fields.
728#[derive(Debug, Clone)]
729pub struct InputKeyMap {
730    /// Accept autocomplete suggestion.
731    pub accept_suggestion: Binding,
732    /// Go to next field.
733    pub next: Binding,
734    /// Go to previous field.
735    pub prev: Binding,
736    /// Submit the form.
737    pub submit: Binding,
738}
739
740impl Default for InputKeyMap {
741    fn default() -> Self {
742        Self {
743            accept_suggestion: Binding::new().keys(&["ctrl+e"]).help("ctrl+e", "complete"),
744            prev: Binding::new()
745                .keys(&["shift+tab"])
746                .help("shift+tab", "back"),
747            next: Binding::new().keys(&["enter", "tab"]).help("enter", "next"),
748            submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
749        }
750    }
751}
752
753/// Keybindings for select fields.
754#[derive(Debug, Clone)]
755pub struct SelectKeyMap {
756    /// Go to next field.
757    pub next: Binding,
758    /// Go to previous field.
759    pub prev: Binding,
760    /// Move cursor up.
761    pub up: Binding,
762    /// Move cursor down.
763    pub down: Binding,
764    /// Move cursor left (inline mode).
765    pub left: Binding,
766    /// Move cursor right (inline mode).
767    pub right: Binding,
768    /// Open filter.
769    pub filter: Binding,
770    /// Apply filter.
771    pub set_filter: Binding,
772    /// Clear filter.
773    pub clear_filter: Binding,
774    /// Half page up.
775    pub half_page_up: Binding,
776    /// Half page down.
777    pub half_page_down: Binding,
778    /// Go to top.
779    pub goto_top: Binding,
780    /// Go to bottom.
781    pub goto_bottom: Binding,
782    /// Submit the form.
783    pub submit: Binding,
784}
785
786impl Default for SelectKeyMap {
787    fn default() -> Self {
788        Self {
789            prev: Binding::new()
790                .keys(&["shift+tab"])
791                .help("shift+tab", "back"),
792            next: Binding::new()
793                .keys(&["enter", "tab"])
794                .help("enter", "select"),
795            submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
796            up: Binding::new()
797                .keys(&["up", "k", "ctrl+k", "ctrl+p"])
798                .help("↑", "up"),
799            down: Binding::new()
800                .keys(&["down", "j", "ctrl+j", "ctrl+n"])
801                .help("↓", "down"),
802            left: Binding::new()
803                .keys(&["h", "left"])
804                .help("←", "left")
805                .set_enabled(false),
806            right: Binding::new()
807                .keys(&["l", "right"])
808                .help("→", "right")
809                .set_enabled(false),
810            filter: Binding::new().keys(&["/"]).help("/", "filter"),
811            set_filter: Binding::new()
812                .keys(&["escape"])
813                .help("esc", "set filter")
814                .set_enabled(false),
815            clear_filter: Binding::new()
816                .keys(&["escape"])
817                .help("esc", "clear filter")
818                .set_enabled(false),
819            half_page_up: Binding::new().keys(&["ctrl+u"]).help("ctrl+u", "½ page up"),
820            half_page_down: Binding::new()
821                .keys(&["ctrl+d"])
822                .help("ctrl+d", "½ page down"),
823            goto_top: Binding::new()
824                .keys(&["home", "g"])
825                .help("g/home", "go to start"),
826            goto_bottom: Binding::new()
827                .keys(&["end", "G"])
828                .help("G/end", "go to end"),
829        }
830    }
831}
832
833/// Keybindings for multi-select fields.
834#[derive(Debug, Clone)]
835pub struct MultiSelectKeyMap {
836    /// Go to next field.
837    pub next: Binding,
838    /// Go to previous field.
839    pub prev: Binding,
840    /// Move cursor up.
841    pub up: Binding,
842    /// Move cursor down.
843    pub down: Binding,
844    /// Toggle selection.
845    pub toggle: Binding,
846    /// Open filter.
847    pub filter: Binding,
848    /// Apply filter.
849    pub set_filter: Binding,
850    /// Clear filter.
851    pub clear_filter: Binding,
852    /// Half page up.
853    pub half_page_up: Binding,
854    /// Half page down.
855    pub half_page_down: Binding,
856    /// Go to top.
857    pub goto_top: Binding,
858    /// Go to bottom.
859    pub goto_bottom: Binding,
860    /// Select all.
861    pub select_all: Binding,
862    /// Select none.
863    pub select_none: Binding,
864    /// Submit the form.
865    pub submit: Binding,
866}
867
868impl Default for MultiSelectKeyMap {
869    fn default() -> Self {
870        Self {
871            prev: Binding::new()
872                .keys(&["shift+tab"])
873                .help("shift+tab", "back"),
874            next: Binding::new()
875                .keys(&["enter", "tab"])
876                .help("enter", "confirm"),
877            submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
878            toggle: Binding::new().keys(&[" ", "x"]).help("x", "toggle"),
879            up: Binding::new().keys(&["up", "k", "ctrl+p"]).help("↑", "up"),
880            down: Binding::new()
881                .keys(&["down", "j", "ctrl+n"])
882                .help("↓", "down"),
883            filter: Binding::new().keys(&["/"]).help("/", "filter"),
884            set_filter: Binding::new()
885                .keys(&["enter", "escape"])
886                .help("esc", "set filter")
887                .set_enabled(false),
888            clear_filter: Binding::new()
889                .keys(&["escape"])
890                .help("esc", "clear filter")
891                .set_enabled(false),
892            half_page_up: Binding::new().keys(&["ctrl+u"]).help("ctrl+u", "½ page up"),
893            half_page_down: Binding::new()
894                .keys(&["ctrl+d"])
895                .help("ctrl+d", "½ page down"),
896            goto_top: Binding::new()
897                .keys(&["home", "g"])
898                .help("g/home", "go to start"),
899            goto_bottom: Binding::new()
900                .keys(&["end", "G"])
901                .help("G/end", "go to end"),
902            select_all: Binding::new()
903                .keys(&["ctrl+a"])
904                .help("ctrl+a", "select all"),
905            select_none: Binding::new()
906                .keys(&["ctrl+a"])
907                .help("ctrl+a", "select none")
908                .set_enabled(false),
909        }
910    }
911}
912
913/// Keybindings for confirm fields.
914#[derive(Debug, Clone)]
915pub struct ConfirmKeyMap {
916    /// Go to next field.
917    pub next: Binding,
918    /// Go to previous field.
919    pub prev: Binding,
920    /// Toggle between yes/no.
921    pub toggle: Binding,
922    /// Submit the form.
923    pub submit: Binding,
924    /// Accept (yes).
925    pub accept: Binding,
926    /// Reject (no).
927    pub reject: Binding,
928}
929
930impl Default for ConfirmKeyMap {
931    fn default() -> Self {
932        Self {
933            prev: Binding::new()
934                .keys(&["shift+tab"])
935                .help("shift+tab", "back"),
936            next: Binding::new().keys(&["enter", "tab"]).help("enter", "next"),
937            submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
938            toggle: Binding::new()
939                .keys(&["h", "l", "right", "left"])
940                .help("←/→", "toggle"),
941            accept: Binding::new().keys(&["y", "Y"]).help("y", "Yes"),
942            reject: Binding::new().keys(&["n", "N"]).help("n", "No"),
943        }
944    }
945}
946
947/// Keybindings for note fields.
948#[derive(Debug, Clone)]
949pub struct NoteKeyMap {
950    /// Go to next field.
951    pub next: Binding,
952    /// Go to previous field.
953    pub prev: Binding,
954    /// Submit the form.
955    pub submit: Binding,
956}
957
958impl Default for NoteKeyMap {
959    fn default() -> Self {
960        Self {
961            prev: Binding::new()
962                .keys(&["shift+tab"])
963                .help("shift+tab", "back"),
964            next: Binding::new().keys(&["enter", "tab"]).help("enter", "next"),
965            submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
966        }
967    }
968}
969
970/// Keybindings for text area fields.
971#[derive(Debug, Clone)]
972pub struct TextKeyMap {
973    /// Go to next field.
974    pub next: Binding,
975    /// Go to previous field.
976    pub prev: Binding,
977    /// Insert a new line.
978    pub new_line: Binding,
979    /// Open external editor.
980    pub editor: Binding,
981    /// Submit the form.
982    pub submit: Binding,
983    /// Uppercase word forward.
984    pub uppercase_word_forward: Binding,
985    /// Lowercase word forward.
986    pub lowercase_word_forward: Binding,
987    /// Capitalize word forward.
988    pub capitalize_word_forward: Binding,
989    /// Transpose character backward.
990    pub transpose_character_backward: Binding,
991}
992
993impl Default for TextKeyMap {
994    fn default() -> Self {
995        Self {
996            prev: Binding::new()
997                .keys(&["shift+tab"])
998                .help("shift+tab", "back"),
999            next: Binding::new().keys(&["tab", "enter"]).help("enter", "next"),
1000            submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
1001            new_line: Binding::new()
1002                .keys(&["alt+enter", "ctrl+j"])
1003                .help("alt+enter / ctrl+j", "new line"),
1004            editor: Binding::new()
1005                .keys(&["ctrl+e"])
1006                .help("ctrl+e", "open editor"),
1007            uppercase_word_forward: Binding::new()
1008                .keys(&["alt+u"])
1009                .help("alt+u", "uppercase word"),
1010            lowercase_word_forward: Binding::new()
1011                .keys(&["alt+l"])
1012                .help("alt+l", "lowercase word"),
1013            capitalize_word_forward: Binding::new()
1014                .keys(&["alt+c"])
1015                .help("alt+c", "capitalize word"),
1016            transpose_character_backward: Binding::new()
1017                .keys(&["ctrl+t"])
1018                .help("ctrl+t", "transpose"),
1019        }
1020    }
1021}
1022
1023/// Keybindings for file picker fields.
1024#[derive(Debug, Clone)]
1025pub struct FilePickerKeyMap {
1026    /// Go to next field.
1027    pub next: Binding,
1028    /// Go to previous field.
1029    pub prev: Binding,
1030    /// Submit the form.
1031    pub submit: Binding,
1032    /// Move up in file list.
1033    pub up: Binding,
1034    /// Move down in file list.
1035    pub down: Binding,
1036    /// Open directory or select file.
1037    pub open: Binding,
1038    /// Close picker / go back.
1039    pub close: Binding,
1040    /// Go back to parent directory.
1041    pub back: Binding,
1042    /// Select current item.
1043    pub select: Binding,
1044    /// Go to top of list.
1045    pub goto_top: Binding,
1046    /// Go to bottom of list.
1047    pub goto_bottom: Binding,
1048    /// Page up.
1049    pub page_up: Binding,
1050    /// Page down.
1051    pub page_down: Binding,
1052}
1053
1054impl Default for FilePickerKeyMap {
1055    fn default() -> Self {
1056        Self {
1057            prev: Binding::new()
1058                .keys(&["shift+tab"])
1059                .help("shift+tab", "back"),
1060            next: Binding::new().keys(&["tab"]).help("tab", "next"),
1061            submit: Binding::new().keys(&["enter"]).help("enter", "submit"),
1062            up: Binding::new().keys(&["up", "k"]).help("↑/k", "up"),
1063            down: Binding::new().keys(&["down", "j"]).help("↓/j", "down"),
1064            open: Binding::new().keys(&["enter", "l"]).help("enter", "open"),
1065            close: Binding::new().keys(&["esc", "q"]).help("esc", "close"),
1066            back: Binding::new().keys(&["backspace", "h"]).help("h", "back"),
1067            select: Binding::new().keys(&["enter"]).help("enter", "select"),
1068            goto_top: Binding::new().keys(&["g"]).help("g", "first"),
1069            goto_bottom: Binding::new().keys(&["G"]).help("G", "last"),
1070            page_up: Binding::new().keys(&["pgup", "K"]).help("pgup", "page up"),
1071            page_down: Binding::new()
1072                .keys(&["pgdown", "J"])
1073                .help("pgdown", "page down"),
1074        }
1075    }
1076}
1077
1078// -----------------------------------------------------------------------------
1079// Field Position
1080// -----------------------------------------------------------------------------
1081
1082/// Positional information about a field within a form.
1083#[derive(Debug, Clone, Copy, Default)]
1084pub struct FieldPosition {
1085    /// Current group index.
1086    pub group: usize,
1087    /// Current field index within group.
1088    pub field: usize,
1089    /// First non-skipped field index.
1090    pub first_field: usize,
1091    /// Last non-skipped field index.
1092    pub last_field: usize,
1093    /// Total number of groups.
1094    pub group_count: usize,
1095    /// First non-hidden group index.
1096    pub first_group: usize,
1097    /// Last non-hidden group index.
1098    pub last_group: usize,
1099}
1100
1101impl FieldPosition {
1102    /// Returns whether this field is the first in the form.
1103    pub fn is_first(&self) -> bool {
1104        self.field == self.first_field && self.group == self.first_group
1105    }
1106
1107    /// Returns whether this field is the last in the form.
1108    pub fn is_last(&self) -> bool {
1109        self.field == self.last_field && self.group == self.last_group
1110    }
1111}
1112
1113// -----------------------------------------------------------------------------
1114// Helper for key matching
1115// -----------------------------------------------------------------------------
1116
1117/// Check if a KeyMsg matches a Binding.
1118fn binding_matches(binding: &Binding, key: &KeyMsg) -> bool {
1119    if !binding.enabled() {
1120        return false;
1121    }
1122    let key_str = key.to_string();
1123    binding.get_keys().iter().any(|k| k == &key_str)
1124}
1125
1126// -----------------------------------------------------------------------------
1127// Field Trait
1128// -----------------------------------------------------------------------------
1129
1130/// A form field.
1131pub trait Field: Send + Sync {
1132    /// Returns the field's key.
1133    fn get_key(&self) -> &str;
1134
1135    /// Returns the field's value.
1136    fn get_value(&self) -> Box<dyn Any>;
1137
1138    /// Returns whether this field should be skipped.
1139    fn skip(&self) -> bool {
1140        false
1141    }
1142
1143    /// Returns whether this field should zoom (take full height).
1144    fn zoom(&self) -> bool {
1145        false
1146    }
1147
1148    /// Returns the current validation error, if any.
1149    fn error(&self) -> Option<&str>;
1150
1151    /// Initializes the field.
1152    fn init(&mut self) -> Option<Cmd>;
1153
1154    /// Updates the field with a message.
1155    fn update(&mut self, msg: &Message) -> Option<Cmd>;
1156
1157    /// Renders the field.
1158    fn view(&self) -> String;
1159
1160    /// Focuses the field.
1161    fn focus(&mut self) -> Option<Cmd>;
1162
1163    /// Blurs the field.
1164    fn blur(&mut self) -> Option<Cmd>;
1165
1166    /// Returns the help keybindings.
1167    fn key_binds(&self) -> Vec<Binding>;
1168
1169    /// Sets the theme.
1170    fn with_theme(&mut self, theme: &Theme);
1171
1172    /// Sets the keymap.
1173    fn with_keymap(&mut self, keymap: &KeyMap);
1174
1175    /// Sets the width.
1176    fn with_width(&mut self, width: usize);
1177
1178    /// Sets the height.
1179    fn with_height(&mut self, height: usize);
1180
1181    /// Sets the field position.
1182    fn with_position(&mut self, position: FieldPosition);
1183}
1184
1185// -----------------------------------------------------------------------------
1186// Messages
1187// -----------------------------------------------------------------------------
1188
1189/// Message to move to the next field.
1190#[derive(Debug, Clone)]
1191pub struct NextFieldMsg;
1192
1193/// Message to move to the previous field.
1194#[derive(Debug, Clone)]
1195pub struct PrevFieldMsg;
1196
1197/// Message to move to the next group.
1198#[derive(Debug, Clone)]
1199pub struct NextGroupMsg;
1200
1201/// Message to move to the previous group.
1202#[derive(Debug, Clone)]
1203pub struct PrevGroupMsg;
1204
1205/// Message to update dynamic field content.
1206#[derive(Debug, Clone)]
1207pub struct UpdateFieldMsg;
1208
1209// -----------------------------------------------------------------------------
1210// Input Field
1211// -----------------------------------------------------------------------------
1212
1213/// A text input field.
1214pub struct Input {
1215    id: usize,
1216    key: String,
1217    value: String,
1218    title: String,
1219    description: String,
1220    placeholder: String,
1221    prompt: String,
1222    char_limit: usize,
1223    echo_mode: EchoMode,
1224    inline: bool,
1225    focused: bool,
1226    error: Option<String>,
1227    validate: Option<fn(&str) -> Option<String>>,
1228    width: usize,
1229    _height: usize,
1230    theme: Option<Theme>,
1231    keymap: InputKeyMap,
1232    _position: FieldPosition,
1233    cursor_pos: usize,
1234    suggestions: Vec<String>,
1235    show_suggestions: bool,
1236}
1237
1238/// Echo mode for input fields.
1239#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1240pub enum EchoMode {
1241    /// Display text as-is.
1242    #[default]
1243    Normal,
1244    /// Display mask characters (for passwords).
1245    Password,
1246    /// Display nothing.
1247    None,
1248}
1249
1250impl Default for Input {
1251    fn default() -> Self {
1252        Self::new()
1253    }
1254}
1255
1256impl Input {
1257    /// Creates a new input field.
1258    pub fn new() -> Self {
1259        Self {
1260            id: next_id(),
1261            key: String::new(),
1262            value: String::new(),
1263            title: String::new(),
1264            description: String::new(),
1265            placeholder: String::new(),
1266            prompt: "> ".to_string(),
1267            char_limit: 0,
1268            echo_mode: EchoMode::Normal,
1269            inline: false,
1270            focused: false,
1271            error: None,
1272            validate: None,
1273            width: 80,
1274            _height: 0,
1275            theme: None,
1276            keymap: InputKeyMap::default(),
1277            _position: FieldPosition::default(),
1278            cursor_pos: 0,
1279            suggestions: Vec::new(),
1280            show_suggestions: false,
1281        }
1282    }
1283
1284    /// Sets the field key.
1285    pub fn key(mut self, key: impl Into<String>) -> Self {
1286        self.key = key.into();
1287        self
1288    }
1289
1290    /// Sets the initial value.
1291    pub fn value(mut self, value: impl Into<String>) -> Self {
1292        self.value = value.into();
1293        self.cursor_pos = self.value.chars().count();
1294        self
1295    }
1296
1297    /// Sets the title.
1298    pub fn title(mut self, title: impl Into<String>) -> Self {
1299        self.title = title.into();
1300        self
1301    }
1302
1303    /// Sets the description.
1304    pub fn description(mut self, description: impl Into<String>) -> Self {
1305        self.description = description.into();
1306        self
1307    }
1308
1309    /// Sets the placeholder text.
1310    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
1311        self.placeholder = placeholder.into();
1312        self
1313    }
1314
1315    /// Sets the prompt string.
1316    pub fn prompt(mut self, prompt: impl Into<String>) -> Self {
1317        self.prompt = prompt.into();
1318        self
1319    }
1320
1321    /// Sets the character limit.
1322    pub fn char_limit(mut self, limit: usize) -> Self {
1323        self.char_limit = limit;
1324        self
1325    }
1326
1327    /// Sets the echo mode.
1328    pub fn echo_mode(mut self, mode: EchoMode) -> Self {
1329        self.echo_mode = mode;
1330        self
1331    }
1332
1333    /// Sets password mode (shorthand for echo_mode).
1334    pub fn password(self, password: bool) -> Self {
1335        if password {
1336            self.echo_mode(EchoMode::Password)
1337        } else {
1338            self.echo_mode(EchoMode::Normal)
1339        }
1340    }
1341
1342    /// Sets whether the title and input are on the same line.
1343    pub fn inline(mut self, inline: bool) -> Self {
1344        self.inline = inline;
1345        self
1346    }
1347
1348    /// Sets the validation function.
1349    pub fn validate(mut self, validate: fn(&str) -> Option<String>) -> Self {
1350        self.validate = Some(validate);
1351        self
1352    }
1353
1354    /// Sets the suggestions for autocomplete.
1355    pub fn suggestions(mut self, suggestions: Vec<String>) -> Self {
1356        self.suggestions = suggestions;
1357        self.show_suggestions = !self.suggestions.is_empty();
1358        self
1359    }
1360
1361    fn get_theme(&self) -> Theme {
1362        self.theme.clone().unwrap_or_else(theme_charm)
1363    }
1364
1365    fn active_styles(&self) -> FieldStyles {
1366        let theme = self.get_theme();
1367        if self.focused {
1368            theme.focused
1369        } else {
1370            theme.blurred
1371        }
1372    }
1373
1374    fn run_validation(&mut self) {
1375        if let Some(validate) = self.validate {
1376            self.error = validate(&self.value);
1377        }
1378    }
1379
1380    fn display_value(&self) -> String {
1381        match self.echo_mode {
1382            EchoMode::Normal => self.value.clone(),
1383            EchoMode::Password => "•".repeat(self.value.chars().count()),
1384            EchoMode::None => String::new(),
1385        }
1386    }
1387
1388    /// Gets the current value.
1389    pub fn get_string_value(&self) -> &str {
1390        &self.value
1391    }
1392
1393    /// Returns the field ID.
1394    pub fn id(&self) -> usize {
1395        self.id
1396    }
1397}
1398
1399impl Field for Input {
1400    fn get_key(&self) -> &str {
1401        &self.key
1402    }
1403
1404    fn get_value(&self) -> Box<dyn Any> {
1405        Box::new(self.value.clone())
1406    }
1407
1408    fn error(&self) -> Option<&str> {
1409        self.error.as_deref()
1410    }
1411
1412    fn init(&mut self) -> Option<Cmd> {
1413        None
1414    }
1415
1416    fn update(&mut self, msg: &Message) -> Option<Cmd> {
1417        if !self.focused {
1418            return None;
1419        }
1420
1421        if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
1422            self.error = None;
1423
1424            // Check for prev
1425            if binding_matches(&self.keymap.prev, key_msg) {
1426                return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
1427            }
1428
1429            // Check for next/submit
1430            if binding_matches(&self.keymap.next, key_msg)
1431                || binding_matches(&self.keymap.submit, key_msg)
1432            {
1433                self.run_validation();
1434                if self.error.is_some() {
1435                    return None;
1436                }
1437                return Some(Cmd::new(|| Message::new(NextFieldMsg)));
1438            }
1439
1440            // Handle character input
1441            // Note: cursor_pos is a character index (not byte index) for proper Unicode support
1442            match key_msg.key_type {
1443                KeyType::Runes => {
1444                    // Preprocess paste content: for single-line inputs, collapse newlines/tabs to spaces
1445                    let chars_to_insert: Vec<char> = if key_msg.paste {
1446                        key_msg
1447                            .runes
1448                            .iter()
1449                            .map(|&c| {
1450                                if c == '\n' || c == '\r' || c == '\t' {
1451                                    ' '
1452                                } else {
1453                                    c
1454                                }
1455                            })
1456                            // Collapse multiple consecutive spaces into one
1457                            .fold(Vec::new(), |mut acc, c| {
1458                                if c == ' ' && acc.last() == Some(&' ') {
1459                                    // Skip duplicate space
1460                                } else {
1461                                    acc.push(c);
1462                                }
1463                                acc
1464                            })
1465                    } else {
1466                        key_msg.runes.clone()
1467                    };
1468
1469                    // Calculate how many chars we can insert respecting char_limit
1470                    let current_count = self.value.chars().count();
1471                    let available = if self.char_limit == 0 {
1472                        usize::MAX
1473                    } else {
1474                        self.char_limit.saturating_sub(current_count)
1475                    };
1476                    let chars_to_add: Vec<char> =
1477                        chars_to_insert.into_iter().take(available).collect();
1478
1479                    if !chars_to_add.is_empty() {
1480                        // Convert character position to byte position for insertion
1481                        let byte_pos = self
1482                            .value
1483                            .char_indices()
1484                            .nth(self.cursor_pos)
1485                            .map(|(i, _)| i)
1486                            .unwrap_or(self.value.len());
1487
1488                        // Build the new string efficiently for bulk insert
1489                        let insert_str: String = chars_to_add.iter().collect();
1490                        self.value.insert_str(byte_pos, &insert_str);
1491                        self.cursor_pos += chars_to_add.len();
1492                    }
1493                }
1494                KeyType::Backspace => {
1495                    if self.cursor_pos > 0 {
1496                        self.cursor_pos -= 1;
1497                        // Convert character position to byte position for removal
1498                        if let Some((byte_pos, _)) = self.value.char_indices().nth(self.cursor_pos)
1499                        {
1500                            self.value.remove(byte_pos);
1501                        }
1502                    }
1503                }
1504                KeyType::Delete => {
1505                    let char_count = self.value.chars().count();
1506                    if self.cursor_pos < char_count {
1507                        // Convert character position to byte position for removal
1508                        if let Some((byte_pos, _)) = self.value.char_indices().nth(self.cursor_pos)
1509                        {
1510                            self.value.remove(byte_pos);
1511                        }
1512                    }
1513                }
1514                KeyType::Left => {
1515                    if self.cursor_pos > 0 {
1516                        self.cursor_pos -= 1;
1517                    }
1518                }
1519                KeyType::Right => {
1520                    let char_count = self.value.chars().count();
1521                    if self.cursor_pos < char_count {
1522                        self.cursor_pos += 1;
1523                    }
1524                }
1525                KeyType::Home => {
1526                    self.cursor_pos = 0;
1527                }
1528                KeyType::End => {
1529                    self.cursor_pos = self.value.chars().count();
1530                }
1531                _ => {}
1532            }
1533        }
1534
1535        None
1536    }
1537
1538    fn view(&self) -> String {
1539        let styles = self.active_styles();
1540        let mut output = String::new();
1541
1542        // Title
1543        if !self.title.is_empty() {
1544            output.push_str(&styles.title.render(&self.title));
1545            if !self.inline {
1546                output.push('\n');
1547            }
1548        }
1549
1550        // Description
1551        if !self.description.is_empty() {
1552            output.push_str(&styles.description.render(&self.description));
1553            if !self.inline {
1554                output.push('\n');
1555            }
1556        }
1557
1558        // Prompt and value
1559        output.push_str(&styles.text_input.prompt.render(&self.prompt));
1560
1561        let display = self.display_value();
1562        if display.is_empty() && !self.placeholder.is_empty() {
1563            output.push_str(&styles.text_input.placeholder.render(&self.placeholder));
1564        } else {
1565            output.push_str(&styles.text_input.text.render(&display));
1566        }
1567
1568        // Error indicator
1569        if self.error.is_some() {
1570            output.push_str(&styles.error_indicator.render(""));
1571        }
1572
1573        styles
1574            .base
1575            .width(self.width.try_into().unwrap_or(u16::MAX))
1576            .render(&output)
1577    }
1578
1579    fn focus(&mut self) -> Option<Cmd> {
1580        self.focused = true;
1581        None
1582    }
1583
1584    fn blur(&mut self) -> Option<Cmd> {
1585        self.focused = false;
1586        self.run_validation();
1587        None
1588    }
1589
1590    fn key_binds(&self) -> Vec<Binding> {
1591        if self.show_suggestions {
1592            vec![
1593                self.keymap.accept_suggestion.clone(),
1594                self.keymap.prev.clone(),
1595                self.keymap.submit.clone(),
1596                self.keymap.next.clone(),
1597            ]
1598        } else {
1599            vec![
1600                self.keymap.prev.clone(),
1601                self.keymap.submit.clone(),
1602                self.keymap.next.clone(),
1603            ]
1604        }
1605    }
1606
1607    fn with_theme(&mut self, theme: &Theme) {
1608        if self.theme.is_none() {
1609            self.theme = Some(theme.clone());
1610        }
1611    }
1612
1613    fn with_keymap(&mut self, keymap: &KeyMap) {
1614        self.keymap = keymap.input.clone();
1615    }
1616
1617    fn with_width(&mut self, width: usize) {
1618        self.width = width;
1619    }
1620
1621    fn with_height(&mut self, height: usize) {
1622        self._height = height;
1623    }
1624
1625    fn with_position(&mut self, position: FieldPosition) {
1626        self._position = position;
1627    }
1628}
1629
1630// -----------------------------------------------------------------------------
1631// Select Field
1632// -----------------------------------------------------------------------------
1633
1634/// A select field for choosing one option from a list.
1635pub struct Select<T: Clone + PartialEq + Send + Sync + 'static> {
1636    id: usize,
1637    key: String,
1638    options: Vec<SelectOption<T>>,
1639    selected: usize,
1640    title: String,
1641    description: String,
1642    inline: bool,
1643    focused: bool,
1644    error: Option<String>,
1645    validate: Option<fn(&T) -> Option<String>>,
1646    width: usize,
1647    height: usize,
1648    theme: Option<Theme>,
1649    keymap: SelectKeyMap,
1650    _position: FieldPosition,
1651    filtering: bool,
1652    filter_value: String,
1653    offset: usize,
1654}
1655
1656impl<T: Clone + PartialEq + Send + Sync + Default + 'static> Default for Select<T> {
1657    fn default() -> Self {
1658        Self::new()
1659    }
1660}
1661
1662impl<T: Clone + PartialEq + Send + Sync + Default + 'static> Select<T> {
1663    /// Creates a new select field.
1664    pub fn new() -> Self {
1665        Self {
1666            id: next_id(),
1667            key: String::new(),
1668            options: Vec::new(),
1669            selected: 0,
1670            title: String::new(),
1671            description: String::new(),
1672            inline: false,
1673            focused: false,
1674            error: None,
1675            validate: None,
1676            width: 80,
1677            height: 5,
1678            theme: None,
1679            keymap: SelectKeyMap::default(),
1680            _position: FieldPosition::default(),
1681            filtering: false,
1682            filter_value: String::new(),
1683            offset: 0,
1684        }
1685    }
1686
1687    /// Sets the field key.
1688    pub fn key(mut self, key: impl Into<String>) -> Self {
1689        self.key = key.into();
1690        self
1691    }
1692
1693    /// Sets the options.
1694    pub fn options(mut self, options: Vec<SelectOption<T>>) -> Self {
1695        self.options = options;
1696        // Find initially selected
1697        for (i, opt) in self.options.iter().enumerate() {
1698            if opt.selected {
1699                self.selected = i;
1700                break;
1701            }
1702        }
1703        self
1704    }
1705
1706    /// Sets the title.
1707    pub fn title(mut self, title: impl Into<String>) -> Self {
1708        self.title = title.into();
1709        self
1710    }
1711
1712    /// Sets the description.
1713    pub fn description(mut self, description: impl Into<String>) -> Self {
1714        self.description = description.into();
1715        self
1716    }
1717
1718    /// Sets whether options display inline.
1719    pub fn inline(mut self, inline: bool) -> Self {
1720        self.inline = inline;
1721        self
1722    }
1723
1724    /// Sets the validation function.
1725    pub fn validate(mut self, validate: fn(&T) -> Option<String>) -> Self {
1726        self.validate = Some(validate);
1727        self
1728    }
1729
1730    /// Sets the visible height (number of options shown).
1731    pub fn height_options(mut self, height: usize) -> Self {
1732        self.height = height;
1733        self
1734    }
1735
1736    /// Enables or disables type-to-filter support.
1737    ///
1738    /// When filtering is enabled, typing characters will filter the visible
1739    /// options. Navigation keys (j/k/g/G) still work for movement.
1740    /// Press Escape to clear the filter, Backspace to delete the last character.
1741    pub fn filterable(mut self, enabled: bool) -> Self {
1742        self.filtering = enabled;
1743        self
1744    }
1745
1746    /// Updates the filter value and adjusts the selection to stay on the same
1747    /// item when possible, or clamps to valid bounds if the current item is
1748    /// filtered out.
1749    fn update_filter(&mut self, new_value: String) {
1750        // Remember what item `selected` is currently pointing to (original index)
1751        let current_item_idx = self.selected;
1752
1753        // Update the filter
1754        self.filter_value = new_value;
1755
1756        // Collect filtered indices into owned vec to avoid borrow conflicts
1757        let filtered_indices: Vec<usize> = self.filtered_indices();
1758
1759        // Try to keep selection on the same original item
1760        if filtered_indices.contains(&current_item_idx) {
1761            // Item still visible — keep selection
1762            self.adjust_offset_from_indices(&filtered_indices);
1763            return;
1764        }
1765
1766        // Item no longer visible — select the first filtered item (or keep 0)
1767        if let Some(&first_idx) = filtered_indices.first() {
1768            self.selected = first_idx;
1769        }
1770        self.adjust_offset_from_indices(&filtered_indices);
1771    }
1772
1773    /// Returns just the original indices of filtered options (owned data,
1774    /// no borrows on self).
1775    fn filtered_indices(&self) -> Vec<usize> {
1776        if self.filter_value.is_empty() {
1777            (0..self.options.len()).collect()
1778        } else {
1779            let filter_lower = self.filter_value.to_lowercase();
1780            self.options
1781                .iter()
1782                .enumerate()
1783                .filter(|(_, o)| o.key.to_lowercase().contains(&filter_lower))
1784                .map(|(i, _)| i)
1785                .collect()
1786        }
1787    }
1788
1789    /// Adjusts the scroll offset to keep the current selection visible
1790    /// within the filtered view.
1791    fn adjust_offset_from_indices(&mut self, filtered_indices: &[usize]) {
1792        let pos = filtered_indices
1793            .iter()
1794            .position(|&idx| idx == self.selected)
1795            .unwrap_or(0);
1796        if pos < self.offset {
1797            self.offset = pos;
1798        } else if pos >= self.offset + self.height {
1799            self.offset = pos.saturating_sub(self.height.saturating_sub(1));
1800        }
1801    }
1802
1803    fn get_theme(&self) -> Theme {
1804        self.theme.clone().unwrap_or_else(theme_charm)
1805    }
1806
1807    fn active_styles(&self) -> FieldStyles {
1808        let theme = self.get_theme();
1809        if self.focused {
1810            theme.focused
1811        } else {
1812            theme.blurred
1813        }
1814    }
1815
1816    fn run_validation(&mut self) {
1817        if let Some(validate) = self.validate
1818            && let Some(opt) = self.options.get(self.selected)
1819        {
1820            self.error = validate(&opt.value);
1821        }
1822    }
1823
1824    fn filtered_options(&self) -> Vec<(usize, &SelectOption<T>)> {
1825        if self.filter_value.is_empty() {
1826            self.options.iter().enumerate().collect()
1827        } else {
1828            let filter_lower = self.filter_value.to_lowercase();
1829            self.options
1830                .iter()
1831                .enumerate()
1832                .filter(|(_, o)| o.key.to_lowercase().contains(&filter_lower))
1833                .collect()
1834        }
1835    }
1836
1837    /// Gets the currently selected value.
1838    pub fn get_selected_value(&self) -> Option<&T> {
1839        self.options.get(self.selected).map(|o| &o.value)
1840    }
1841
1842    /// Returns the field ID.
1843    pub fn id(&self) -> usize {
1844        self.id
1845    }
1846}
1847
1848impl<T: Clone + PartialEq + Send + Sync + Default + 'static> Field for Select<T> {
1849    fn get_key(&self) -> &str {
1850        &self.key
1851    }
1852
1853    fn get_value(&self) -> Box<dyn Any> {
1854        if let Some(opt) = self.options.get(self.selected) {
1855            Box::new(opt.value.clone())
1856        } else {
1857            Box::new(T::default())
1858        }
1859    }
1860
1861    fn error(&self) -> Option<&str> {
1862        self.error.as_deref()
1863    }
1864
1865    fn init(&mut self) -> Option<Cmd> {
1866        None
1867    }
1868
1869    fn update(&mut self, msg: &Message) -> Option<Cmd> {
1870        if !self.focused {
1871            return None;
1872        }
1873
1874        if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
1875            self.error = None;
1876
1877            // Handle filter input when filtering is enabled
1878            if self.filtering {
1879                // Clear filter on Escape
1880                if key_msg.key_type == KeyType::Esc {
1881                    self.update_filter(String::new());
1882                    return None;
1883                }
1884
1885                // Remove character on Backspace
1886                if key_msg.key_type == KeyType::Backspace {
1887                    if !self.filter_value.is_empty() {
1888                        let mut new_filter = self.filter_value.clone();
1889                        new_filter.pop();
1890                        self.update_filter(new_filter);
1891                    }
1892                    return None;
1893                }
1894
1895                // Add characters to filter (skip navigation keys)
1896                if key_msg.key_type == KeyType::Runes {
1897                    let mut new_filter = self.filter_value.clone();
1898                    for c in &key_msg.runes {
1899                        // Skip navigation/action keys so they still work
1900                        match c {
1901                            'j' | 'k' | 'g' | 'G' | '/' => continue,
1902                            _ => {}
1903                        }
1904                        if c.is_alphanumeric() || c.is_whitespace() || c.is_ascii_punctuation() {
1905                            new_filter.push(*c);
1906                        }
1907                    }
1908                    if new_filter != self.filter_value {
1909                        self.update_filter(new_filter);
1910                        return None;
1911                    }
1912                }
1913            }
1914
1915            // Check for prev
1916            if binding_matches(&self.keymap.prev, key_msg) {
1917                return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
1918            }
1919
1920            // Check for next/submit
1921            if binding_matches(&self.keymap.next, key_msg)
1922                || binding_matches(&self.keymap.submit, key_msg)
1923            {
1924                self.run_validation();
1925                if self.error.is_some() {
1926                    return None;
1927                }
1928                return Some(Cmd::new(|| Message::new(NextFieldMsg)));
1929            }
1930
1931            // Navigation operates on the filtered list.
1932            // Collect indices into owned vec to avoid borrow conflicts.
1933            let filtered_indices = self.filtered_indices();
1934            let current_pos = filtered_indices
1935                .iter()
1936                .position(|&idx| idx == self.selected);
1937
1938            if binding_matches(&self.keymap.up, key_msg)
1939                && let Some(pos) = current_pos
1940                && pos > 0
1941            {
1942                self.selected = filtered_indices[pos - 1];
1943                self.adjust_offset_from_indices(&filtered_indices);
1944            } else if binding_matches(&self.keymap.down, key_msg)
1945                && let Some(pos) = current_pos
1946                && pos < filtered_indices.len().saturating_sub(1)
1947            {
1948                self.selected = filtered_indices[pos + 1];
1949                self.adjust_offset_from_indices(&filtered_indices);
1950            } else if binding_matches(&self.keymap.goto_top, key_msg)
1951                && let Some(&idx) = filtered_indices.first()
1952            {
1953                self.selected = idx;
1954                self.offset = 0;
1955            } else if binding_matches(&self.keymap.goto_bottom, key_msg)
1956                && let Some(&idx) = filtered_indices.last()
1957            {
1958                self.selected = idx;
1959                let last_pos = filtered_indices.len().saturating_sub(1);
1960                self.offset = last_pos.saturating_sub(self.height.saturating_sub(1));
1961            }
1962        }
1963
1964        None
1965    }
1966
1967    fn view(&self) -> String {
1968        let styles = self.active_styles();
1969        let mut output = String::new();
1970
1971        // Title
1972        if !self.title.is_empty() {
1973            output.push_str(&styles.title.render(&self.title));
1974            output.push('\n');
1975        }
1976
1977        // Description
1978        if !self.description.is_empty() {
1979            output.push_str(&styles.description.render(&self.description));
1980            output.push('\n');
1981        }
1982
1983        // Filter input (if filtering is enabled and filter is active)
1984        if self.filtering && !self.filter_value.is_empty() {
1985            let filter_display = format!("Filter: {}_", self.filter_value);
1986            output.push_str(&styles.description.render(&filter_display));
1987            output.push('\n');
1988        }
1989
1990        // Options
1991        let filtered = self.filtered_options();
1992        let visible: Vec<_> = filtered
1993            .iter()
1994            .skip(self.offset)
1995            .take(self.height)
1996            .collect();
1997
1998        if self.inline {
1999            // Inline mode
2000            let mut inline_output = String::new();
2001            inline_output.push_str(&styles.prev_indicator.render(""));
2002            for (i, (idx, opt)) in visible.iter().enumerate() {
2003                if *idx == self.selected {
2004                    inline_output.push_str(&styles.selected_option.render(&opt.key));
2005                } else {
2006                    inline_output.push_str(&styles.option.render(&opt.key));
2007                }
2008                if i < visible.len() - 1 {
2009                    inline_output.push_str("  ");
2010                }
2011            }
2012            inline_output.push_str(&styles.next_indicator.render(""));
2013            output.push_str(&inline_output);
2014        } else {
2015            // Vertical list mode
2016            let has_visible = !visible.is_empty();
2017            for (idx, opt) in &visible {
2018                if *idx == self.selected {
2019                    output.push_str(&styles.select_selector.render(""));
2020                    output.push_str(&styles.selected_option.render(&opt.key));
2021                } else {
2022                    output.push_str("  ");
2023                    output.push_str(&styles.option.render(&opt.key));
2024                }
2025                output.push('\n');
2026            }
2027            // Remove trailing newline
2028            if has_visible {
2029                output.pop();
2030            }
2031        }
2032
2033        // Error indicator
2034        if self.error.is_some() {
2035            output.push_str(&styles.error_indicator.render(""));
2036        }
2037
2038        styles
2039            .base
2040            .width(self.width.try_into().unwrap_or(u16::MAX))
2041            .render(&output)
2042    }
2043
2044    fn focus(&mut self) -> Option<Cmd> {
2045        self.focused = true;
2046        None
2047    }
2048
2049    fn blur(&mut self) -> Option<Cmd> {
2050        self.focused = false;
2051        self.run_validation();
2052        None
2053    }
2054
2055    fn key_binds(&self) -> Vec<Binding> {
2056        vec![
2057            self.keymap.up.clone(),
2058            self.keymap.down.clone(),
2059            self.keymap.prev.clone(),
2060            self.keymap.submit.clone(),
2061            self.keymap.next.clone(),
2062        ]
2063    }
2064
2065    fn with_theme(&mut self, theme: &Theme) {
2066        if self.theme.is_none() {
2067            self.theme = Some(theme.clone());
2068        }
2069    }
2070
2071    fn with_keymap(&mut self, keymap: &KeyMap) {
2072        self.keymap = keymap.select.clone();
2073    }
2074
2075    fn with_width(&mut self, width: usize) {
2076        self.width = width;
2077    }
2078
2079    fn with_height(&mut self, height: usize) {
2080        self.height = height;
2081    }
2082
2083    fn with_position(&mut self, position: FieldPosition) {
2084        self._position = position;
2085    }
2086}
2087
2088// -----------------------------------------------------------------------------
2089// MultiSelect Field
2090// -----------------------------------------------------------------------------
2091
2092/// A multi-select field for choosing multiple options from a list.
2093pub struct MultiSelect<T: Clone + PartialEq + Send + Sync + 'static> {
2094    id: usize,
2095    key: String,
2096    options: Vec<SelectOption<T>>,
2097    selected: Vec<usize>,
2098    cursor: usize,
2099    title: String,
2100    description: String,
2101    focused: bool,
2102    error: Option<String>,
2103    #[allow(clippy::type_complexity)]
2104    validate: Option<fn(&[T]) -> Option<String>>,
2105    width: usize,
2106    height: usize,
2107    limit: Option<usize>,
2108    theme: Option<Theme>,
2109    keymap: MultiSelectKeyMap,
2110    _position: FieldPosition,
2111    filtering: bool,
2112    filter_value: String,
2113    offset: usize,
2114}
2115
2116impl<T: Clone + PartialEq + Send + Sync + Default + 'static> Default for MultiSelect<T> {
2117    fn default() -> Self {
2118        Self::new()
2119    }
2120}
2121
2122impl<T: Clone + PartialEq + Send + Sync + Default + 'static> MultiSelect<T> {
2123    /// Creates a new multi-select field.
2124    pub fn new() -> Self {
2125        Self {
2126            id: next_id(),
2127            key: String::new(),
2128            options: Vec::new(),
2129            selected: Vec::new(),
2130            cursor: 0,
2131            title: String::new(),
2132            description: String::new(),
2133            focused: false,
2134            error: None,
2135            validate: None,
2136            width: 80,
2137            height: 5,
2138            limit: None,
2139            theme: None,
2140            keymap: MultiSelectKeyMap::default(),
2141            _position: FieldPosition::default(),
2142            filtering: false,
2143            filter_value: String::new(),
2144            offset: 0,
2145        }
2146    }
2147
2148    /// Sets the field key.
2149    pub fn key(mut self, key: impl Into<String>) -> Self {
2150        self.key = key.into();
2151        self
2152    }
2153
2154    /// Sets the options.
2155    pub fn options(mut self, options: Vec<SelectOption<T>>) -> Self {
2156        self.options = options;
2157        // Find initially selected options
2158        self.selected = self
2159            .options
2160            .iter()
2161            .enumerate()
2162            .filter(|(_, opt)| opt.selected)
2163            .map(|(i, _)| i)
2164            .collect();
2165        self
2166    }
2167
2168    /// Sets the title.
2169    pub fn title(mut self, title: impl Into<String>) -> Self {
2170        self.title = title.into();
2171        self
2172    }
2173
2174    /// Sets the description.
2175    pub fn description(mut self, description: impl Into<String>) -> Self {
2176        self.description = description.into();
2177        self
2178    }
2179
2180    /// Sets the validation function.
2181    pub fn validate(mut self, validate: fn(&[T]) -> Option<String>) -> Self {
2182        self.validate = Some(validate);
2183        self
2184    }
2185
2186    /// Sets the visible height (number of options shown).
2187    pub fn height_options(mut self, height: usize) -> Self {
2188        self.height = height;
2189        self
2190    }
2191
2192    /// Sets the maximum number of selections allowed.
2193    pub fn limit(mut self, limit: usize) -> Self {
2194        self.limit = Some(limit);
2195        self
2196    }
2197
2198    /// Enables or disables filtering mode.
2199    ///
2200    /// When enabled, pressing '/' enters filter mode where typing filters options.
2201    pub fn filterable(mut self, enabled: bool) -> Self {
2202        self.filtering = enabled;
2203        self
2204    }
2205
2206    /// Updates the filter value with proper cursor adjustment.
2207    ///
2208    /// This method ensures the cursor stays on the same item when possible,
2209    /// or clamps to valid bounds if the current item is filtered out.
2210    fn update_filter(&mut self, new_value: String) {
2211        // Remember what item cursor is currently pointing to (original index)
2212        let old_filtered = self.filtered_options();
2213        let current_item_idx = old_filtered.get(self.cursor).map(|(idx, _)| *idx);
2214
2215        // Update the filter
2216        self.filter_value = new_value;
2217
2218        // Recalculate filtered options
2219        let new_filtered = self.filtered_options();
2220
2221        // Try to keep cursor on the same item
2222        if let Some(item_idx) = current_item_idx
2223            && let Some(new_pos) = new_filtered.iter().position(|(idx, _)| *idx == item_idx)
2224        {
2225            self.cursor = new_pos;
2226            self.adjust_offset();
2227            return;
2228        }
2229
2230        // Item no longer visible, clamp cursor to valid range
2231        self.cursor = self.cursor.min(new_filtered.len().saturating_sub(1));
2232        self.adjust_offset();
2233    }
2234
2235    /// Adjusts the offset to keep the cursor visible within the view.
2236    fn adjust_offset(&mut self) {
2237        // Ensure cursor is within visible window
2238        if self.cursor < self.offset {
2239            self.offset = self.cursor;
2240        } else if self.cursor >= self.offset + self.height {
2241            self.offset = self.cursor.saturating_sub(self.height.saturating_sub(1));
2242        }
2243    }
2244
2245    fn get_theme(&self) -> Theme {
2246        self.theme.clone().unwrap_or_else(theme_charm)
2247    }
2248
2249    fn active_styles(&self) -> FieldStyles {
2250        let theme = self.get_theme();
2251        if self.focused {
2252            theme.focused
2253        } else {
2254            theme.blurred
2255        }
2256    }
2257
2258    fn run_validation(&mut self) {
2259        if let Some(validate) = self.validate {
2260            let values: Vec<T> = self
2261                .selected
2262                .iter()
2263                .filter_map(|&i| self.options.get(i).map(|o| o.value.clone()))
2264                .collect();
2265            self.error = validate(&values);
2266        }
2267    }
2268
2269    fn filtered_options(&self) -> Vec<(usize, &SelectOption<T>)> {
2270        if self.filter_value.is_empty() {
2271            self.options.iter().enumerate().collect()
2272        } else {
2273            let filter_lower = self.filter_value.to_lowercase();
2274            self.options
2275                .iter()
2276                .enumerate()
2277                .filter(|(_, o)| o.key.to_lowercase().contains(&filter_lower))
2278                .collect()
2279        }
2280    }
2281
2282    fn toggle_current(&mut self) {
2283        let filtered = self.filtered_options();
2284        if let Some((idx, _)) = filtered.get(self.cursor) {
2285            if let Some(pos) = self.selected.iter().position(|&i| i == *idx) {
2286                // Deselect
2287                self.selected.remove(pos);
2288            } else if self.limit.is_none_or(|l| self.selected.len() < l) {
2289                // Select (if within limit)
2290                self.selected.push(*idx);
2291            }
2292        }
2293    }
2294
2295    fn select_all(&mut self) {
2296        if let Some(limit) = self.limit {
2297            // Only select up to limit
2298            self.selected = self
2299                .options
2300                .iter()
2301                .enumerate()
2302                .take(limit)
2303                .map(|(i, _)| i)
2304                .collect();
2305        } else {
2306            self.selected = (0..self.options.len()).collect();
2307        }
2308    }
2309
2310    fn select_none(&mut self) {
2311        self.selected.clear();
2312    }
2313
2314    /// Gets the currently selected values.
2315    pub fn get_selected_values(&self) -> Vec<&T> {
2316        self.selected
2317            .iter()
2318            .filter_map(|&i| self.options.get(i).map(|o| &o.value))
2319            .collect()
2320    }
2321
2322    /// Returns the field ID.
2323    pub fn id(&self) -> usize {
2324        self.id
2325    }
2326}
2327
2328impl<T: Clone + PartialEq + Send + Sync + Default + 'static> Field for MultiSelect<T> {
2329    fn get_key(&self) -> &str {
2330        &self.key
2331    }
2332
2333    fn get_value(&self) -> Box<dyn Any> {
2334        let values: Vec<T> = self
2335            .selected
2336            .iter()
2337            .filter_map(|&i| self.options.get(i).map(|o| o.value.clone()))
2338            .collect();
2339        Box::new(values)
2340    }
2341
2342    fn error(&self) -> Option<&str> {
2343        self.error.as_deref()
2344    }
2345
2346    fn init(&mut self) -> Option<Cmd> {
2347        None
2348    }
2349
2350    fn update(&mut self, msg: &Message) -> Option<Cmd> {
2351        if !self.focused {
2352            return None;
2353        }
2354
2355        if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
2356            self.error = None;
2357
2358            // Handle filter input when filtering is enabled
2359            if self.filtering {
2360                // Clear filter on Escape
2361                if key_msg.key_type == KeyType::Esc {
2362                    self.update_filter(String::new());
2363                    return None;
2364                }
2365
2366                // Remove character on Backspace
2367                if key_msg.key_type == KeyType::Backspace {
2368                    if !self.filter_value.is_empty() {
2369                        let mut new_filter = self.filter_value.clone();
2370                        new_filter.pop();
2371                        self.update_filter(new_filter);
2372                    }
2373                    return None;
2374                }
2375
2376                // Add characters to filter
2377                if key_msg.key_type == KeyType::Runes {
2378                    let mut new_filter = self.filter_value.clone();
2379                    for c in &key_msg.runes {
2380                        // Only add printable characters that aren't navigation/toggle keys
2381                        // Always skip these keys so they work for navigation/toggle
2382                        match c {
2383                            'j' | 'k' | 'g' | 'G' | ' ' | 'x' | '/' => continue,
2384                            _ => {}
2385                        }
2386                        if c.is_alphanumeric() || c.is_whitespace() || c.is_ascii_punctuation() {
2387                            new_filter.push(*c);
2388                        }
2389                    }
2390                    if new_filter != self.filter_value {
2391                        self.update_filter(new_filter);
2392                        return None;
2393                    }
2394                }
2395            }
2396
2397            // Check for prev
2398            if binding_matches(&self.keymap.prev, key_msg) {
2399                return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
2400            }
2401
2402            // Check for next/submit
2403            if binding_matches(&self.keymap.next, key_msg)
2404                || binding_matches(&self.keymap.submit, key_msg)
2405            {
2406                self.run_validation();
2407                if self.error.is_some() {
2408                    return None;
2409                }
2410                return Some(Cmd::new(|| Message::new(NextFieldMsg)));
2411            }
2412
2413            // Toggle selection
2414            if binding_matches(&self.keymap.toggle, key_msg) {
2415                self.toggle_current();
2416            }
2417
2418            // Select all
2419            if binding_matches(&self.keymap.select_all, key_msg) {
2420                if self.selected.len() == self.options.len() {
2421                    self.select_none();
2422                } else {
2423                    self.select_all();
2424                }
2425            }
2426
2427            // Navigation
2428            if binding_matches(&self.keymap.up, key_msg) {
2429                if self.cursor > 0 {
2430                    self.cursor -= 1;
2431                    if self.cursor < self.offset {
2432                        self.offset = self.cursor;
2433                    }
2434                }
2435            } else if binding_matches(&self.keymap.down, key_msg) {
2436                let filtered = self.filtered_options();
2437                if self.cursor < filtered.len().saturating_sub(1) {
2438                    self.cursor += 1;
2439                    if self.cursor >= self.offset + self.height {
2440                        self.offset = self.cursor.saturating_sub(self.height.saturating_sub(1));
2441                    }
2442                }
2443            } else if binding_matches(&self.keymap.goto_top, key_msg) {
2444                self.cursor = 0;
2445                self.offset = 0;
2446            } else if binding_matches(&self.keymap.goto_bottom, key_msg) {
2447                let filtered = self.filtered_options();
2448                self.cursor = filtered.len().saturating_sub(1);
2449                self.offset = self.cursor.saturating_sub(self.height.saturating_sub(1));
2450            }
2451        }
2452
2453        None
2454    }
2455
2456    fn view(&self) -> String {
2457        let styles = self.active_styles();
2458        let mut output = String::new();
2459
2460        // Title
2461        if !self.title.is_empty() {
2462            output.push_str(&styles.title.render(&self.title));
2463            output.push('\n');
2464        }
2465
2466        // Description
2467        if !self.description.is_empty() {
2468            output.push_str(&styles.description.render(&self.description));
2469            output.push('\n');
2470        }
2471
2472        // Filter input (if filtering is enabled and filter is active)
2473        if self.filtering && !self.filter_value.is_empty() {
2474            let filter_display = format!("Filter: {}_", self.filter_value);
2475            output.push_str(&styles.description.render(&filter_display));
2476            output.push('\n');
2477        }
2478
2479        // Options
2480        let filtered = self.filtered_options();
2481        let visible: Vec<_> = filtered
2482            .iter()
2483            .skip(self.offset)
2484            .take(self.height)
2485            .collect();
2486
2487        // Vertical list mode with checkboxes
2488        for (i, (idx, opt)) in visible.iter().enumerate() {
2489            let is_cursor = self.offset + i == self.cursor;
2490            let is_selected = self.selected.contains(idx);
2491
2492            // Cursor indicator
2493            if is_cursor {
2494                output.push_str(&styles.select_selector.render(""));
2495            } else {
2496                output.push_str("  ");
2497            }
2498
2499            // Checkbox
2500            let checkbox = if is_selected { "[x] " } else { "[ ] " };
2501            output.push_str(checkbox);
2502
2503            // Option text
2504            if is_cursor {
2505                output.push_str(&styles.selected_option.render(&opt.key));
2506            } else {
2507                output.push_str(&styles.option.render(&opt.key));
2508            }
2509
2510            output.push('\n');
2511        }
2512
2513        // Remove trailing newline
2514        if !visible.is_empty() {
2515            output.pop();
2516        }
2517
2518        // Error indicator
2519        if self.error.is_some() {
2520            output.push_str(&styles.error_indicator.render(""));
2521        }
2522
2523        styles
2524            .base
2525            .width(self.width.try_into().unwrap_or(u16::MAX))
2526            .render(&output)
2527    }
2528
2529    fn focus(&mut self) -> Option<Cmd> {
2530        self.focused = true;
2531        None
2532    }
2533
2534    fn blur(&mut self) -> Option<Cmd> {
2535        self.focused = false;
2536        self.run_validation();
2537        None
2538    }
2539
2540    fn key_binds(&self) -> Vec<Binding> {
2541        vec![
2542            self.keymap.up.clone(),
2543            self.keymap.down.clone(),
2544            self.keymap.toggle.clone(),
2545            self.keymap.prev.clone(),
2546            self.keymap.submit.clone(),
2547            self.keymap.next.clone(),
2548        ]
2549    }
2550
2551    fn with_theme(&mut self, theme: &Theme) {
2552        if self.theme.is_none() {
2553            self.theme = Some(theme.clone());
2554        }
2555    }
2556
2557    fn with_keymap(&mut self, keymap: &KeyMap) {
2558        self.keymap = keymap.multi_select.clone();
2559    }
2560
2561    fn with_width(&mut self, width: usize) {
2562        self.width = width;
2563    }
2564
2565    fn with_height(&mut self, height: usize) {
2566        self.height = height;
2567    }
2568
2569    fn with_position(&mut self, position: FieldPosition) {
2570        self._position = position;
2571    }
2572}
2573
2574// -----------------------------------------------------------------------------
2575// Confirm Field
2576// -----------------------------------------------------------------------------
2577
2578/// A confirmation field with Yes/No options.
2579pub struct Confirm {
2580    id: usize,
2581    key: String,
2582    value: bool,
2583    title: String,
2584    description: String,
2585    affirmative: String,
2586    negative: String,
2587    focused: bool,
2588    width: usize,
2589    theme: Option<Theme>,
2590    keymap: ConfirmKeyMap,
2591    _position: FieldPosition,
2592}
2593
2594impl Default for Confirm {
2595    fn default() -> Self {
2596        Self::new()
2597    }
2598}
2599
2600impl Confirm {
2601    /// Creates a new confirm field.
2602    pub fn new() -> Self {
2603        Self {
2604            id: next_id(),
2605            key: String::new(),
2606            value: false,
2607            title: String::new(),
2608            description: String::new(),
2609            affirmative: "Yes".to_string(),
2610            negative: "No".to_string(),
2611            focused: false,
2612            width: 80,
2613            theme: None,
2614            keymap: ConfirmKeyMap::default(),
2615            _position: FieldPosition::default(),
2616        }
2617    }
2618
2619    /// Sets the field key.
2620    pub fn key(mut self, key: impl Into<String>) -> Self {
2621        self.key = key.into();
2622        self
2623    }
2624
2625    /// Sets the initial value.
2626    pub fn value(mut self, value: bool) -> Self {
2627        self.value = value;
2628        self
2629    }
2630
2631    /// Sets the title.
2632    pub fn title(mut self, title: impl Into<String>) -> Self {
2633        self.title = title.into();
2634        self
2635    }
2636
2637    /// Sets the description.
2638    pub fn description(mut self, description: impl Into<String>) -> Self {
2639        self.description = description.into();
2640        self
2641    }
2642
2643    /// Sets the affirmative button text.
2644    pub fn affirmative(mut self, text: impl Into<String>) -> Self {
2645        self.affirmative = text.into();
2646        self
2647    }
2648
2649    /// Sets the negative button text.
2650    pub fn negative(mut self, text: impl Into<String>) -> Self {
2651        self.negative = text.into();
2652        self
2653    }
2654
2655    fn get_theme(&self) -> Theme {
2656        self.theme.clone().unwrap_or_else(theme_charm)
2657    }
2658
2659    fn active_styles(&self) -> FieldStyles {
2660        let theme = self.get_theme();
2661        if self.focused {
2662            theme.focused
2663        } else {
2664            theme.blurred
2665        }
2666    }
2667
2668    /// Gets the current value.
2669    pub fn get_bool_value(&self) -> bool {
2670        self.value
2671    }
2672
2673    /// Returns the field ID.
2674    pub fn id(&self) -> usize {
2675        self.id
2676    }
2677}
2678
2679impl Field for Confirm {
2680    fn get_key(&self) -> &str {
2681        &self.key
2682    }
2683
2684    fn get_value(&self) -> Box<dyn Any> {
2685        Box::new(self.value)
2686    }
2687
2688    fn error(&self) -> Option<&str> {
2689        None
2690    }
2691
2692    fn init(&mut self) -> Option<Cmd> {
2693        None
2694    }
2695
2696    fn update(&mut self, msg: &Message) -> Option<Cmd> {
2697        if !self.focused {
2698            return None;
2699        }
2700
2701        if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
2702            // Check for prev
2703            if binding_matches(&self.keymap.prev, key_msg) {
2704                return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
2705            }
2706
2707            // Check for next/submit
2708            if binding_matches(&self.keymap.next, key_msg)
2709                || binding_matches(&self.keymap.submit, key_msg)
2710            {
2711                return Some(Cmd::new(|| Message::new(NextFieldMsg)));
2712            }
2713
2714            // Toggle
2715            if binding_matches(&self.keymap.toggle, key_msg) {
2716                self.value = !self.value;
2717            }
2718
2719            // Direct accept/reject
2720            if binding_matches(&self.keymap.accept, key_msg) {
2721                self.value = true;
2722            }
2723            if binding_matches(&self.keymap.reject, key_msg) {
2724                self.value = false;
2725            }
2726        }
2727
2728        None
2729    }
2730
2731    fn view(&self) -> String {
2732        let styles = self.active_styles();
2733        let mut output = String::new();
2734
2735        // Title
2736        if !self.title.is_empty() {
2737            output.push_str(&styles.title.render(&self.title));
2738            output.push('\n');
2739        }
2740
2741        // Description
2742        if !self.description.is_empty() {
2743            output.push_str(&styles.description.render(&self.description));
2744            output.push('\n');
2745        }
2746
2747        // Buttons
2748        if self.value {
2749            output.push_str(&styles.focused_button.render(&self.affirmative));
2750            output.push_str(&styles.blurred_button.render(&self.negative));
2751        } else {
2752            output.push_str(&styles.blurred_button.render(&self.affirmative));
2753            output.push_str(&styles.focused_button.render(&self.negative));
2754        }
2755
2756        styles
2757            .base
2758            .width(self.width.try_into().unwrap_or(u16::MAX))
2759            .render(&output)
2760    }
2761
2762    fn focus(&mut self) -> Option<Cmd> {
2763        self.focused = true;
2764        None
2765    }
2766
2767    fn blur(&mut self) -> Option<Cmd> {
2768        self.focused = false;
2769        None
2770    }
2771
2772    fn key_binds(&self) -> Vec<Binding> {
2773        vec![
2774            self.keymap.toggle.clone(),
2775            self.keymap.accept.clone(),
2776            self.keymap.reject.clone(),
2777            self.keymap.prev.clone(),
2778            self.keymap.submit.clone(),
2779            self.keymap.next.clone(),
2780        ]
2781    }
2782
2783    fn with_theme(&mut self, theme: &Theme) {
2784        if self.theme.is_none() {
2785            self.theme = Some(theme.clone());
2786        }
2787    }
2788
2789    fn with_keymap(&mut self, keymap: &KeyMap) {
2790        self.keymap = keymap.confirm.clone();
2791    }
2792
2793    fn with_width(&mut self, width: usize) {
2794        self.width = width;
2795    }
2796
2797    fn with_height(&mut self, _height: usize) {
2798        // Confirm doesn't use height
2799    }
2800
2801    fn with_position(&mut self, position: FieldPosition) {
2802        self._position = position;
2803    }
2804}
2805
2806// -----------------------------------------------------------------------------
2807// Note Field
2808// -----------------------------------------------------------------------------
2809
2810/// A non-interactive note/text display field.
2811pub struct Note {
2812    id: usize,
2813    key: String,
2814    title: String,
2815    description: String,
2816    focused: bool,
2817    width: usize,
2818    theme: Option<Theme>,
2819    keymap: NoteKeyMap,
2820    _position: FieldPosition,
2821    next_label: String,
2822}
2823
2824impl Default for Note {
2825    fn default() -> Self {
2826        Self::new()
2827    }
2828}
2829
2830impl Note {
2831    /// Creates a new note field.
2832    pub fn new() -> Self {
2833        Self {
2834            id: next_id(),
2835            key: String::new(),
2836            title: String::new(),
2837            description: String::new(),
2838            focused: false,
2839            width: 80,
2840            theme: None,
2841            keymap: NoteKeyMap::default(),
2842            _position: FieldPosition::default(),
2843            next_label: "Next".to_string(),
2844        }
2845    }
2846
2847    /// Sets the field key.
2848    pub fn key(mut self, key: impl Into<String>) -> Self {
2849        self.key = key.into();
2850        self
2851    }
2852
2853    /// Sets the title.
2854    pub fn title(mut self, title: impl Into<String>) -> Self {
2855        self.title = title.into();
2856        self
2857    }
2858
2859    /// Sets the description (body text).
2860    pub fn description(mut self, description: impl Into<String>) -> Self {
2861        self.description = description.into();
2862        self
2863    }
2864
2865    /// Sets the next button label.
2866    pub fn next_label(mut self, label: impl Into<String>) -> Self {
2867        self.next_label = label.into();
2868        self
2869    }
2870
2871    /// Sets the next button label (alias for `next_label`).
2872    ///
2873    /// This method is provided for compatibility with Go's huh API.
2874    pub fn next(self, label: impl Into<String>) -> Self {
2875        self.next_label(label)
2876    }
2877
2878    fn get_theme(&self) -> Theme {
2879        self.theme.clone().unwrap_or_else(theme_charm)
2880    }
2881
2882    fn active_styles(&self) -> FieldStyles {
2883        let theme = self.get_theme();
2884        if self.focused {
2885            theme.focused
2886        } else {
2887            theme.blurred
2888        }
2889    }
2890
2891    /// Returns the field ID.
2892    pub fn id(&self) -> usize {
2893        self.id
2894    }
2895}
2896
2897impl Field for Note {
2898    fn get_key(&self) -> &str {
2899        &self.key
2900    }
2901
2902    fn get_value(&self) -> Box<dyn Any> {
2903        Box::new(())
2904    }
2905
2906    fn error(&self) -> Option<&str> {
2907        None
2908    }
2909
2910    fn init(&mut self) -> Option<Cmd> {
2911        None
2912    }
2913
2914    fn update(&mut self, msg: &Message) -> Option<Cmd> {
2915        if !self.focused {
2916            return None;
2917        }
2918
2919        if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
2920            // Check for prev
2921            if binding_matches(&self.keymap.prev, key_msg) {
2922                return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
2923            }
2924
2925            // Check for next/submit
2926            if binding_matches(&self.keymap.next, key_msg)
2927                || binding_matches(&self.keymap.submit, key_msg)
2928            {
2929                return Some(Cmd::new(|| Message::new(NextFieldMsg)));
2930            }
2931        }
2932
2933        None
2934    }
2935
2936    fn view(&self) -> String {
2937        let styles = self.active_styles();
2938        let mut output = String::new();
2939
2940        // Title
2941        if !self.title.is_empty() {
2942            output.push_str(&styles.note_title.render(&self.title));
2943            output.push('\n');
2944        }
2945
2946        // Description
2947        if !self.description.is_empty() {
2948            output.push_str(&styles.description.render(&self.description));
2949        }
2950
2951        styles
2952            .base
2953            .width(self.width.try_into().unwrap_or(u16::MAX))
2954            .render(&output)
2955    }
2956
2957    fn focus(&mut self) -> Option<Cmd> {
2958        self.focused = true;
2959        None
2960    }
2961
2962    fn blur(&mut self) -> Option<Cmd> {
2963        self.focused = false;
2964        None
2965    }
2966
2967    fn key_binds(&self) -> Vec<Binding> {
2968        vec![
2969            self.keymap.prev.clone(),
2970            self.keymap.submit.clone(),
2971            self.keymap.next.clone(),
2972        ]
2973    }
2974
2975    fn with_theme(&mut self, theme: &Theme) {
2976        if self.theme.is_none() {
2977            self.theme = Some(theme.clone());
2978        }
2979    }
2980
2981    fn with_keymap(&mut self, keymap: &KeyMap) {
2982        self.keymap = keymap.note.clone();
2983    }
2984
2985    fn with_width(&mut self, width: usize) {
2986        self.width = width;
2987    }
2988
2989    fn with_height(&mut self, _height: usize) {
2990        // Note doesn't use height
2991    }
2992
2993    fn with_position(&mut self, position: FieldPosition) {
2994        self._position = position;
2995    }
2996}
2997
2998// -----------------------------------------------------------------------------
2999// Text Field (Textarea)
3000// -----------------------------------------------------------------------------
3001
3002/// A multi-line text area field.
3003///
3004/// The Text field is used for gathering longer-form user input.
3005/// It wraps the bubbles textarea component and integrates it with the huh form system.
3006///
3007/// # Example
3008///
3009/// ```rust,ignore
3010/// use huh::Text;
3011///
3012/// let text = Text::new()
3013///     .key("bio")
3014///     .title("Biography")
3015///     .description("Tell us about yourself")
3016///     .placeholder("Enter your bio...")
3017///     .lines(5);
3018/// ```
3019pub struct Text {
3020    id: usize,
3021    key: String,
3022    value: String,
3023    title: String,
3024    description: String,
3025    placeholder: String,
3026    lines: usize,
3027    char_limit: usize,
3028    show_line_numbers: bool,
3029    focused: bool,
3030    error: Option<String>,
3031    validate: Option<fn(&str) -> Option<String>>,
3032    width: usize,
3033    height: usize,
3034    theme: Option<Theme>,
3035    keymap: TextKeyMap,
3036    _position: FieldPosition,
3037    cursor_row: usize,
3038    cursor_col: usize,
3039}
3040
3041impl Default for Text {
3042    fn default() -> Self {
3043        Self::new()
3044    }
3045}
3046
3047impl Text {
3048    /// Creates a new text area field.
3049    pub fn new() -> Self {
3050        Self {
3051            id: next_id(),
3052            key: String::new(),
3053            value: String::new(),
3054            title: String::new(),
3055            description: String::new(),
3056            placeholder: String::new(),
3057            lines: 5,
3058            char_limit: 0,
3059            show_line_numbers: false,
3060            focused: false,
3061            error: None,
3062            validate: None,
3063            width: 80,
3064            height: 0,
3065            theme: None,
3066            keymap: TextKeyMap::default(),
3067            _position: FieldPosition::default(),
3068            cursor_row: 0,
3069            cursor_col: 0,
3070        }
3071    }
3072
3073    /// Sets the field key.
3074    pub fn key(mut self, key: impl Into<String>) -> Self {
3075        self.key = key.into();
3076        self
3077    }
3078
3079    /// Sets the initial value.
3080    pub fn value(mut self, value: impl Into<String>) -> Self {
3081        self.value = value.into();
3082        self
3083    }
3084
3085    /// Sets the title.
3086    pub fn title(mut self, title: impl Into<String>) -> Self {
3087        self.title = title.into();
3088        self
3089    }
3090
3091    /// Sets the description.
3092    pub fn description(mut self, description: impl Into<String>) -> Self {
3093        self.description = description.into();
3094        self
3095    }
3096
3097    /// Sets the placeholder text.
3098    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
3099        self.placeholder = placeholder.into();
3100        self
3101    }
3102
3103    /// Sets the number of visible lines.
3104    pub fn lines(mut self, lines: usize) -> Self {
3105        self.lines = lines;
3106        self
3107    }
3108
3109    /// Sets the character limit (0 = no limit).
3110    pub fn char_limit(mut self, limit: usize) -> Self {
3111        self.char_limit = limit;
3112        self
3113    }
3114
3115    /// Sets whether to show line numbers.
3116    pub fn show_line_numbers(mut self, show: bool) -> Self {
3117        self.show_line_numbers = show;
3118        self
3119    }
3120
3121    /// Sets the validation function.
3122    pub fn validate(mut self, validate: fn(&str) -> Option<String>) -> Self {
3123        self.validate = Some(validate);
3124        self
3125    }
3126
3127    fn get_theme(&self) -> Theme {
3128        self.theme.clone().unwrap_or_else(theme_charm)
3129    }
3130
3131    fn active_styles(&self) -> FieldStyles {
3132        let theme = self.get_theme();
3133        if self.focused {
3134            theme.focused
3135        } else {
3136            theme.blurred
3137        }
3138    }
3139
3140    fn run_validation(&mut self) {
3141        if let Some(validate) = self.validate {
3142            self.error = validate(&self.value);
3143        }
3144    }
3145
3146    /// Gets the current value.
3147    pub fn get_string_value(&self) -> &str {
3148        &self.value
3149    }
3150
3151    /// Returns the field ID.
3152    pub fn id(&self) -> usize {
3153        self.id
3154    }
3155
3156    fn visible_lines(&self) -> Vec<&str> {
3157        let lines: Vec<&str> = self.value.lines().collect();
3158        if lines.is_empty() { vec![""] } else { lines }
3159    }
3160
3161    /// Transpose the character at cursor with the one before it.
3162    ///
3163    /// If at the end of the line, moves cursor back first. After swapping,
3164    /// moves cursor right if not at end of line. No-op if cursor is at
3165    /// beginning of line or line has fewer than 2 characters.
3166    fn transpose_left(&mut self) {
3167        let lines: Vec<String> = self.value.lines().map(String::from).collect();
3168        if self.cursor_row >= lines.len() {
3169            return;
3170        }
3171
3172        let line_chars: Vec<char> = lines[self.cursor_row].chars().collect();
3173
3174        // No-op if at beginning or line too short
3175        if self.cursor_col == 0 || line_chars.len() < 2 {
3176            return;
3177        }
3178
3179        let mut col = self.cursor_col;
3180
3181        // If at end, move back first
3182        if col >= line_chars.len() {
3183            col = line_chars.len() - 1;
3184            self.cursor_col = col;
3185        }
3186
3187        // Swap chars at col-1 and col
3188        let mut new_chars = line_chars;
3189        new_chars.swap(col - 1, col);
3190
3191        // Rebuild value
3192        let mut new_lines = lines;
3193        new_lines[self.cursor_row] = new_chars.into_iter().collect();
3194        self.value = new_lines.join("\n");
3195
3196        // Move right if not at end of line
3197        let new_line_len = self
3198            .value
3199            .lines()
3200            .nth(self.cursor_row)
3201            .map(|l| l.chars().count())
3202            .unwrap_or(0);
3203        if self.cursor_col < new_line_len {
3204            self.cursor_col += 1;
3205        }
3206    }
3207
3208    /// Helper for word operations - operates on current line.
3209    ///
3210    /// Skips whitespace forward, then processes each character in the word
3211    /// using the provided function. Moves cursor to the end of the word.
3212    fn do_word_right<F>(&mut self, mut f: F)
3213    where
3214        F: FnMut(usize, char) -> char,
3215    {
3216        let lines: Vec<String> = self.value.lines().map(String::from).collect();
3217        if self.cursor_row >= lines.len() {
3218            return;
3219        }
3220
3221        let mut chars: Vec<char> = lines[self.cursor_row].chars().collect();
3222        let len = chars.len();
3223
3224        // Skip spaces forward
3225        while self.cursor_col < len && chars[self.cursor_col].is_whitespace() {
3226            self.cursor_col += 1;
3227        }
3228
3229        // Process word chars
3230        let mut char_idx = 0;
3231        while self.cursor_col < len && !chars[self.cursor_col].is_whitespace() {
3232            chars[self.cursor_col] = f(char_idx, chars[self.cursor_col]);
3233            self.cursor_col += 1;
3234            char_idx += 1;
3235        }
3236
3237        // Rebuild value
3238        let mut new_lines = lines;
3239        new_lines[self.cursor_row] = chars.into_iter().collect();
3240        self.value = new_lines.join("\n");
3241    }
3242
3243    /// Uppercase the word to the right of the cursor.
3244    fn uppercase_right(&mut self) {
3245        self.do_word_right(|_, c| c.to_uppercase().next().unwrap_or(c));
3246    }
3247
3248    /// Lowercase the word to the right of the cursor.
3249    fn lowercase_right(&mut self) {
3250        self.do_word_right(|_, c| c.to_lowercase().next().unwrap_or(c));
3251    }
3252
3253    /// Capitalize the word to the right (first char uppercase, rest unchanged).
3254    fn capitalize_right(&mut self) {
3255        self.do_word_right(|idx, c| {
3256            if idx == 0 {
3257                c.to_uppercase().next().unwrap_or(c)
3258            } else {
3259                c
3260            }
3261        });
3262    }
3263}
3264
3265impl Field for Text {
3266    fn get_key(&self) -> &str {
3267        &self.key
3268    }
3269
3270    fn get_value(&self) -> Box<dyn Any> {
3271        Box::new(self.value.clone())
3272    }
3273
3274    fn error(&self) -> Option<&str> {
3275        self.error.as_deref()
3276    }
3277
3278    fn init(&mut self) -> Option<Cmd> {
3279        None
3280    }
3281
3282    fn update(&mut self, msg: &Message) -> Option<Cmd> {
3283        if !self.focused {
3284            return None;
3285        }
3286
3287        if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
3288            self.error = None;
3289
3290            // Check for prev
3291            if binding_matches(&self.keymap.prev, key_msg) {
3292                return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
3293            }
3294
3295            // Check for next/submit (tab submits in text area)
3296            if binding_matches(&self.keymap.next, key_msg)
3297                || binding_matches(&self.keymap.submit, key_msg)
3298            {
3299                self.run_validation();
3300                if self.error.is_some() {
3301                    return None;
3302                }
3303                return Some(Cmd::new(|| Message::new(NextFieldMsg)));
3304            }
3305
3306            // Check for new line
3307            if binding_matches(&self.keymap.new_line, key_msg) {
3308                if self.char_limit == 0 || self.value.len() < self.char_limit {
3309                    self.value.push('\n');
3310                    self.cursor_row += 1;
3311                    self.cursor_col = 0;
3312                }
3313                return None;
3314            }
3315
3316            // Check for word transformation operations
3317            if binding_matches(&self.keymap.uppercase_word_forward, key_msg) {
3318                self.uppercase_right();
3319                return None;
3320            }
3321            if binding_matches(&self.keymap.lowercase_word_forward, key_msg) {
3322                self.lowercase_right();
3323                return None;
3324            }
3325            if binding_matches(&self.keymap.capitalize_word_forward, key_msg) {
3326                self.capitalize_right();
3327                return None;
3328            }
3329            if binding_matches(&self.keymap.transpose_character_backward, key_msg) {
3330                self.transpose_left();
3331                return None;
3332            }
3333
3334            // Handle text input
3335            match key_msg.key_type {
3336                KeyType::Runes => {
3337                    // Calculate how many chars we can insert respecting char_limit
3338                    let current_count = self.value.chars().count();
3339                    let available = if self.char_limit == 0 {
3340                        usize::MAX
3341                    } else {
3342                        self.char_limit.saturating_sub(current_count)
3343                    };
3344
3345                    // For paste operations, handle bulk insert with proper cursor tracking
3346                    // Multi-line textareas preserve newlines
3347                    let chars_to_add: Vec<char> =
3348                        key_msg.runes.iter().copied().take(available).collect();
3349
3350                    for c in chars_to_add {
3351                        self.value.push(c);
3352                        if c == '\n' {
3353                            self.cursor_row += 1;
3354                            self.cursor_col = 0;
3355                        } else {
3356                            self.cursor_col += 1;
3357                        }
3358                    }
3359                }
3360                KeyType::Backspace => {
3361                    if !self.value.is_empty() {
3362                        let removed = self.value.pop();
3363                        if removed == Some('\n') {
3364                            self.cursor_row = self.cursor_row.saturating_sub(1);
3365                            let lines = self.visible_lines();
3366                            self.cursor_col =
3367                                lines.get(self.cursor_row).map(|l| l.len()).unwrap_or(0);
3368                        } else {
3369                            self.cursor_col = self.cursor_col.saturating_sub(1);
3370                        }
3371                    }
3372                }
3373                KeyType::Enter => {
3374                    // Enter inserts newline in text areas
3375                    if self.char_limit == 0 || self.value.len() < self.char_limit {
3376                        self.value.push('\n');
3377                        self.cursor_row += 1;
3378                        self.cursor_col = 0;
3379                    }
3380                }
3381                KeyType::Up => {
3382                    self.cursor_row = self.cursor_row.saturating_sub(1);
3383                }
3384                KeyType::Down => {
3385                    let line_count = self.visible_lines().len();
3386                    if self.cursor_row < line_count.saturating_sub(1) {
3387                        self.cursor_row += 1;
3388                    }
3389                }
3390                KeyType::Left => {
3391                    if self.cursor_col > 0 {
3392                        self.cursor_col -= 1;
3393                    }
3394                }
3395                KeyType::Right => {
3396                    let lines = self.visible_lines();
3397                    let current_line_len = lines.get(self.cursor_row).map(|l| l.len()).unwrap_or(0);
3398                    if self.cursor_col < current_line_len {
3399                        self.cursor_col += 1;
3400                    }
3401                }
3402                KeyType::Home => {
3403                    self.cursor_col = 0;
3404                }
3405                KeyType::End => {
3406                    let lines = self.visible_lines();
3407                    self.cursor_col = lines.get(self.cursor_row).map(|l| l.len()).unwrap_or(0);
3408                }
3409                _ => {}
3410            }
3411        }
3412
3413        None
3414    }
3415
3416    fn view(&self) -> String {
3417        let styles = self.active_styles();
3418        let mut output = String::new();
3419
3420        // Title
3421        if !self.title.is_empty() {
3422            output.push_str(&styles.title.render(&self.title));
3423            if self.error.is_some() {
3424                output.push_str(&styles.error_indicator.render(""));
3425            }
3426            output.push('\n');
3427        }
3428
3429        // Description
3430        if !self.description.is_empty() {
3431            output.push_str(&styles.description.render(&self.description));
3432            output.push('\n');
3433        }
3434
3435        // Text area content
3436        let lines = self.visible_lines();
3437        let visible_lines = self.lines.min(lines.len().max(1));
3438
3439        for (i, line) in lines.iter().take(visible_lines).enumerate() {
3440            if self.show_line_numbers {
3441                let line_num = format!("{:3} ", i + 1);
3442                output.push_str(&styles.description.render(&line_num));
3443            }
3444
3445            if line.is_empty() && i == 0 && self.value.is_empty() && !self.placeholder.is_empty() {
3446                output.push_str(&styles.text_input.placeholder.render(&self.placeholder));
3447            } else {
3448                output.push_str(&styles.text_input.text.render(line));
3449            }
3450
3451            if i < visible_lines - 1 {
3452                output.push('\n');
3453            }
3454        }
3455
3456        // Pad with empty lines if needed
3457        for i in lines.len()..visible_lines {
3458            output.push('\n');
3459            if self.show_line_numbers {
3460                let line_num = format!("{:3} ", i + 1);
3461                output.push_str(&styles.description.render(&line_num));
3462            }
3463        }
3464
3465        // Error message
3466        if let Some(ref err) = self.error {
3467            output.push('\n');
3468            output.push_str(&styles.error_message.render(err));
3469        }
3470
3471        styles
3472            .base
3473            .width(self.width.try_into().unwrap_or(u16::MAX))
3474            .render(&output)
3475    }
3476
3477    fn focus(&mut self) -> Option<Cmd> {
3478        self.focused = true;
3479        None
3480    }
3481
3482    fn blur(&mut self) -> Option<Cmd> {
3483        self.focused = false;
3484        self.run_validation();
3485        None
3486    }
3487
3488    fn key_binds(&self) -> Vec<Binding> {
3489        vec![
3490            self.keymap.new_line.clone(),
3491            self.keymap.prev.clone(),
3492            self.keymap.submit.clone(),
3493            self.keymap.next.clone(),
3494            self.keymap.uppercase_word_forward.clone(),
3495            self.keymap.lowercase_word_forward.clone(),
3496            self.keymap.capitalize_word_forward.clone(),
3497            self.keymap.transpose_character_backward.clone(),
3498        ]
3499    }
3500
3501    fn with_theme(&mut self, theme: &Theme) {
3502        if self.theme.is_none() {
3503            self.theme = Some(theme.clone());
3504        }
3505    }
3506
3507    fn with_keymap(&mut self, keymap: &KeyMap) {
3508        self.keymap = keymap.text.clone();
3509    }
3510
3511    fn with_width(&mut self, width: usize) {
3512        self.width = width;
3513    }
3514
3515    fn with_height(&mut self, height: usize) {
3516        self.height = height;
3517        // Adjust lines based on height minus title/description
3518        let adjust = if self.title.is_empty() { 0 } else { 1 }
3519            + if self.description.is_empty() { 0 } else { 1 };
3520        if height > adjust {
3521            self.lines = height - adjust;
3522        }
3523    }
3524
3525    fn with_position(&mut self, position: FieldPosition) {
3526        self._position = position;
3527    }
3528}
3529
3530// -----------------------------------------------------------------------------
3531// FilePicker Field
3532// -----------------------------------------------------------------------------
3533
3534/// A file picker field for selecting files and directories.
3535///
3536/// The FilePicker field allows users to browse the filesystem and select files
3537/// or directories. It can be configured to filter by file type, show/hide hidden
3538/// files, and control whether files and/or directories can be selected.
3539///
3540/// # Example
3541///
3542/// ```rust,ignore
3543/// use huh::FilePicker;
3544///
3545/// let picker = FilePicker::new()
3546///     .key("config_file")
3547///     .title("Select Configuration File")
3548///     .description("Choose a .toml or .json file")
3549///     .allowed_types(vec![".toml".to_string(), ".json".to_string()])
3550///     .current_directory(".");
3551/// ```
3552pub struct FilePicker {
3553    id: usize,
3554    key: String,
3555    selected_path: Option<String>,
3556    title: String,
3557    description: String,
3558    current_directory: String,
3559    allowed_types: Vec<String>,
3560    show_hidden: bool,
3561    show_size: bool,
3562    show_permissions: bool,
3563    file_allowed: bool,
3564    dir_allowed: bool,
3565    picking: bool,
3566    focused: bool,
3567    error: Option<String>,
3568    validate: Option<fn(&str) -> Option<String>>,
3569    width: usize,
3570    height: usize,
3571    theme: Option<Theme>,
3572    keymap: FilePickerKeyMap,
3573    _position: FieldPosition,
3574    // Simple file list for display
3575    files: Vec<FileEntry>,
3576    selected_index: usize,
3577    offset: usize,
3578}
3579
3580/// A file entry in the picker.
3581#[derive(Debug, Clone)]
3582struct FileEntry {
3583    name: String,
3584    path: String,
3585    is_dir: bool,
3586    size: u64,
3587    #[allow(dead_code)]
3588    mode: String,
3589}
3590
3591impl Default for FilePicker {
3592    fn default() -> Self {
3593        Self::new()
3594    }
3595}
3596
3597impl FilePicker {
3598    /// Creates a new file picker field.
3599    pub fn new() -> Self {
3600        Self {
3601            id: next_id(),
3602            key: String::new(),
3603            selected_path: None,
3604            title: String::new(),
3605            description: String::new(),
3606            current_directory: ".".to_string(),
3607            allowed_types: Vec::new(),
3608            show_hidden: false,
3609            show_size: false,
3610            show_permissions: false,
3611            file_allowed: true,
3612            dir_allowed: false,
3613            picking: false,
3614            focused: false,
3615            error: None,
3616            validate: None,
3617            width: 80,
3618            height: 10,
3619            theme: None,
3620            keymap: FilePickerKeyMap::default(),
3621            _position: FieldPosition::default(),
3622            files: Vec::new(),
3623            selected_index: 0,
3624            offset: 0,
3625        }
3626    }
3627
3628    /// Sets the field key.
3629    pub fn key(mut self, key: impl Into<String>) -> Self {
3630        self.key = key.into();
3631        self
3632    }
3633
3634    /// Sets the title.
3635    pub fn title(mut self, title: impl Into<String>) -> Self {
3636        self.title = title.into();
3637        self
3638    }
3639
3640    /// Sets the description.
3641    pub fn description(mut self, description: impl Into<String>) -> Self {
3642        self.description = description.into();
3643        self
3644    }
3645
3646    /// Sets the starting directory.
3647    pub fn current_directory(mut self, dir: impl Into<String>) -> Self {
3648        self.current_directory = dir.into();
3649        self
3650    }
3651
3652    /// Sets the allowed file types (extensions).
3653    pub fn allowed_types(mut self, types: Vec<String>) -> Self {
3654        self.allowed_types = types;
3655        self
3656    }
3657
3658    /// Sets whether to show hidden files.
3659    pub fn show_hidden(mut self, show: bool) -> Self {
3660        self.show_hidden = show;
3661        self
3662    }
3663
3664    /// Sets whether to show file sizes.
3665    pub fn show_size(mut self, show: bool) -> Self {
3666        self.show_size = show;
3667        self
3668    }
3669
3670    /// Sets whether to show file permissions.
3671    pub fn show_permissions(mut self, show: bool) -> Self {
3672        self.show_permissions = show;
3673        self
3674    }
3675
3676    /// Sets whether files can be selected.
3677    pub fn file_allowed(mut self, allowed: bool) -> Self {
3678        self.file_allowed = allowed;
3679        self
3680    }
3681
3682    /// Sets whether directories can be selected.
3683    pub fn dir_allowed(mut self, allowed: bool) -> Self {
3684        self.dir_allowed = allowed;
3685        self
3686    }
3687
3688    /// Sets the validation function.
3689    pub fn validate(mut self, validate: fn(&str) -> Option<String>) -> Self {
3690        self.validate = Some(validate);
3691        self
3692    }
3693
3694    /// Sets the visible height (number of entries shown).
3695    pub fn height_entries(mut self, height: usize) -> Self {
3696        self.height = height;
3697        self
3698    }
3699
3700    fn get_theme(&self) -> Theme {
3701        self.theme.clone().unwrap_or_else(theme_charm)
3702    }
3703
3704    fn active_styles(&self) -> FieldStyles {
3705        let theme = self.get_theme();
3706        if self.focused {
3707            theme.focused
3708        } else {
3709            theme.blurred
3710        }
3711    }
3712
3713    fn run_validation(&mut self) {
3714        if let Some(validate) = self.validate
3715            && let Some(ref path) = self.selected_path
3716        {
3717            self.error = validate(path);
3718        }
3719    }
3720
3721    fn read_directory(&mut self) {
3722        self.files.clear();
3723        self.selected_index = 0;
3724        self.offset = 0;
3725
3726        // Add parent directory entry if not at root
3727        if self.current_directory != "/" {
3728            self.files.push(FileEntry {
3729                name: "..".to_string(),
3730                path: "..".to_string(),
3731                is_dir: true,
3732                size: 0,
3733                mode: String::new(),
3734            });
3735        }
3736
3737        // Read directory contents
3738        if let Ok(entries) = std::fs::read_dir(&self.current_directory) {
3739            let mut entries: Vec<_> = entries
3740                .filter_map(|e| e.ok())
3741                .filter_map(|entry| {
3742                    let name = entry.file_name().to_string_lossy().to_string();
3743
3744                    // Skip hidden files unless show_hidden is true
3745                    if !self.show_hidden && name.starts_with('.') {
3746                        return None;
3747                    }
3748
3749                    let metadata = entry.metadata().ok()?;
3750                    let is_dir = metadata.is_dir();
3751                    let size = metadata.len();
3752
3753                    // Filter by allowed types (only for files)
3754                    if !is_dir && !self.allowed_types.is_empty() {
3755                        let matches = self.allowed_types.iter().any(|ext| {
3756                            name.ends_with(ext)
3757                                || name.ends_with(&ext.trim_start_matches('.').to_string())
3758                        });
3759                        if !matches {
3760                            return None;
3761                        }
3762                    }
3763
3764                    let path = entry.path().to_string_lossy().to_string();
3765
3766                    Some(FileEntry {
3767                        name,
3768                        path,
3769                        is_dir,
3770                        size,
3771                        mode: String::new(),
3772                    })
3773                })
3774                .collect();
3775
3776            // Sort: directories first, then alphabetically
3777            entries.sort_by(|a, b| match (a.is_dir, b.is_dir) {
3778                (true, false) => std::cmp::Ordering::Less,
3779                (false, true) => std::cmp::Ordering::Greater,
3780                _ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
3781            });
3782
3783            self.files.extend(entries);
3784        }
3785    }
3786
3787    fn is_selectable(&self, entry: &FileEntry) -> bool {
3788        if entry.is_dir {
3789            self.dir_allowed
3790        } else {
3791            self.file_allowed
3792        }
3793    }
3794
3795    fn format_size(size: u64) -> String {
3796        const KB: u64 = 1024;
3797        const MB: u64 = KB * 1024;
3798        const GB: u64 = MB * 1024;
3799
3800        if size >= GB {
3801            format!("{:.1}G", size as f64 / GB as f64)
3802        } else if size >= MB {
3803            format!("{:.1}M", size as f64 / MB as f64)
3804        } else if size >= KB {
3805            format!("{:.1}K", size as f64 / KB as f64)
3806        } else {
3807            format!("{}B", size)
3808        }
3809    }
3810
3811    /// Gets the currently selected path.
3812    pub fn get_selected_path(&self) -> Option<&str> {
3813        self.selected_path.as_deref()
3814    }
3815
3816    /// Returns the field ID.
3817    pub fn id(&self) -> usize {
3818        self.id
3819    }
3820}
3821
3822impl Field for FilePicker {
3823    fn get_key(&self) -> &str {
3824        &self.key
3825    }
3826
3827    fn get_value(&self) -> Box<dyn Any> {
3828        Box::new(self.selected_path.clone().unwrap_or_default())
3829    }
3830
3831    fn error(&self) -> Option<&str> {
3832        self.error.as_deref()
3833    }
3834
3835    fn init(&mut self) -> Option<Cmd> {
3836        self.read_directory();
3837        None
3838    }
3839
3840    fn update(&mut self, msg: &Message) -> Option<Cmd> {
3841        if !self.focused {
3842            return None;
3843        }
3844
3845        if let Some(key_msg) = msg.downcast_ref::<KeyMsg>() {
3846            self.error = None;
3847
3848            // Check for prev
3849            if binding_matches(&self.keymap.prev, key_msg) {
3850                self.picking = false;
3851                return Some(Cmd::new(|| Message::new(PrevFieldMsg)));
3852            }
3853
3854            // Check for next (tab)
3855            if binding_matches(&self.keymap.next, key_msg) {
3856                self.picking = false;
3857                self.run_validation();
3858                if self.error.is_some() {
3859                    return None;
3860                }
3861                return Some(Cmd::new(|| Message::new(NextFieldMsg)));
3862            }
3863
3864            // Handle close/escape
3865            if binding_matches(&self.keymap.close, key_msg) {
3866                if self.picking {
3867                    self.picking = false;
3868                } else {
3869                    return Some(Cmd::new(|| Message::new(NextFieldMsg)));
3870                }
3871                return None;
3872            }
3873
3874            // Handle open (enter picker mode or select)
3875            if binding_matches(&self.keymap.open, key_msg) {
3876                if !self.picking {
3877                    self.picking = true;
3878                    self.read_directory();
3879                    return None;
3880                }
3881
3882                // In picking mode, open directory or select file
3883                if let Some(entry) = self.files.get(self.selected_index) {
3884                    if entry.name == ".." {
3885                        // Go to parent directory
3886                        if let Some(parent) = std::path::Path::new(&self.current_directory).parent()
3887                        {
3888                            self.current_directory = parent.to_string_lossy().to_string();
3889                            if self.current_directory.is_empty() {
3890                                self.current_directory = "/".to_string();
3891                            }
3892                            self.read_directory();
3893                        }
3894                    } else if entry.is_dir {
3895                        // Enter directory
3896                        self.current_directory = entry.path.clone();
3897                        self.read_directory();
3898                    } else if self.is_selectable(entry) {
3899                        // Select file
3900                        self.selected_path = Some(entry.path.clone());
3901                        self.picking = false;
3902                        self.run_validation();
3903                        if self.error.is_some() {
3904                            return None;
3905                        }
3906                        return Some(Cmd::new(|| Message::new(NextFieldMsg)));
3907                    }
3908                }
3909                return None;
3910            }
3911
3912            // Handle back (go to parent directory)
3913            if self.picking && binding_matches(&self.keymap.back, key_msg) {
3914                if let Some(parent) = std::path::Path::new(&self.current_directory).parent() {
3915                    self.current_directory = parent.to_string_lossy().to_string();
3916                    if self.current_directory.is_empty() {
3917                        self.current_directory = "/".to_string();
3918                    }
3919                    self.read_directory();
3920                }
3921                return None;
3922            }
3923
3924            // Navigation in picker mode
3925            if self.picking {
3926                if binding_matches(&self.keymap.up, key_msg) {
3927                    if self.selected_index > 0 {
3928                        self.selected_index -= 1;
3929                        if self.selected_index < self.offset {
3930                            self.offset = self.selected_index;
3931                        }
3932                    }
3933                } else if binding_matches(&self.keymap.down, key_msg) {
3934                    if !self.files.is_empty()
3935                        && self.selected_index < self.files.len().saturating_sub(1)
3936                    {
3937                        self.selected_index += 1;
3938                        if self.height > 0 && self.selected_index >= self.offset + self.height {
3939                            self.offset = self
3940                                .selected_index
3941                                .saturating_sub(self.height.saturating_sub(1));
3942                        }
3943                    }
3944                } else if binding_matches(&self.keymap.goto_top, key_msg) {
3945                    self.selected_index = 0;
3946                    self.offset = 0;
3947                } else if binding_matches(&self.keymap.goto_bottom, key_msg)
3948                    && !self.files.is_empty()
3949                {
3950                    self.selected_index = self.files.len().saturating_sub(1);
3951                    self.offset = self
3952                        .selected_index
3953                        .saturating_sub(self.height.saturating_sub(1));
3954                }
3955            }
3956        }
3957
3958        None
3959    }
3960
3961    fn view(&self) -> String {
3962        let styles = self.active_styles();
3963        let mut output = String::new();
3964
3965        // Title
3966        if !self.title.is_empty() {
3967            output.push_str(&styles.title.render(&self.title));
3968            if self.error.is_some() {
3969                output.push_str(&styles.error_indicator.render(""));
3970            }
3971            output.push('\n');
3972        }
3973
3974        // Description
3975        if !self.description.is_empty() {
3976            output.push_str(&styles.description.render(&self.description));
3977            output.push('\n');
3978        }
3979
3980        if self.picking {
3981            // Show file list
3982            let visible: Vec<_> = self
3983                .files
3984                .iter()
3985                .skip(self.offset)
3986                .take(self.height)
3987                .collect();
3988
3989            for (i, entry) in visible.iter().enumerate() {
3990                let idx = self.offset + i;
3991                let is_selected = idx == self.selected_index;
3992                let is_selectable = self.is_selectable(entry);
3993
3994                // Cursor
3995                if is_selected {
3996                    output.push_str(&styles.select_selector.render(""));
3997                } else {
3998                    output.push_str("  ");
3999                }
4000
4001                // Entry display
4002                let mut entry_str = String::new();
4003
4004                // Directory/file indicator
4005                if entry.is_dir {
4006                    entry_str.push_str("📁 ");
4007                } else {
4008                    entry_str.push_str("   ");
4009                }
4010
4011                entry_str.push_str(&entry.name);
4012
4013                // Size
4014                if self.show_size && !entry.is_dir {
4015                    entry_str.push_str(&format!(" ({})", Self::format_size(entry.size)));
4016                }
4017
4018                if is_selected && is_selectable {
4019                    output.push_str(&styles.selected_option.render(&entry_str));
4020                } else if !is_selectable && !entry.is_dir && entry.name != ".." {
4021                    output.push_str(&styles.text_input.placeholder.render(&entry_str));
4022                } else {
4023                    output.push_str(&styles.option.render(&entry_str));
4024                }
4025
4026                output.push('\n');
4027            }
4028
4029            // Remove trailing newline
4030            if !visible.is_empty() {
4031                output.pop();
4032            }
4033
4034            // Show current directory
4035            output.push('\n');
4036            output.push_str(
4037                &styles
4038                    .description
4039                    .render(&format!("📂 {}", self.current_directory)),
4040            );
4041        } else {
4042            // Show selected file or placeholder
4043            if let Some(ref path) = self.selected_path {
4044                output.push_str(&styles.selected_option.render(path));
4045            } else {
4046                output.push_str(
4047                    &styles
4048                        .text_input
4049                        .placeholder
4050                        .render("No file selected. Press Enter to browse."),
4051                );
4052            }
4053        }
4054
4055        // Error message
4056        if let Some(ref err) = self.error {
4057            output.push('\n');
4058            output.push_str(&styles.error_message.render(err));
4059        }
4060
4061        styles
4062            .base
4063            .width(self.width.try_into().unwrap_or(u16::MAX))
4064            .render(&output)
4065    }
4066
4067    fn focus(&mut self) -> Option<Cmd> {
4068        self.focused = true;
4069        None
4070    }
4071
4072    fn blur(&mut self) -> Option<Cmd> {
4073        self.focused = false;
4074        self.picking = false;
4075        self.run_validation();
4076        None
4077    }
4078
4079    fn key_binds(&self) -> Vec<Binding> {
4080        if self.picking {
4081            vec![
4082                self.keymap.up.clone(),
4083                self.keymap.down.clone(),
4084                self.keymap.open.clone(),
4085                self.keymap.back.clone(),
4086                self.keymap.close.clone(),
4087            ]
4088        } else {
4089            vec![
4090                self.keymap.open.clone(),
4091                self.keymap.prev.clone(),
4092                self.keymap.next.clone(),
4093            ]
4094        }
4095    }
4096
4097    fn with_theme(&mut self, theme: &Theme) {
4098        if self.theme.is_none() {
4099            self.theme = Some(theme.clone());
4100        }
4101    }
4102
4103    fn with_keymap(&mut self, keymap: &KeyMap) {
4104        self.keymap = keymap.file_picker.clone();
4105    }
4106
4107    fn with_width(&mut self, width: usize) {
4108        self.width = width;
4109    }
4110
4111    fn with_height(&mut self, height: usize) {
4112        self.height = height;
4113    }
4114
4115    fn with_position(&mut self, position: FieldPosition) {
4116        self._position = position;
4117    }
4118}
4119
4120// -----------------------------------------------------------------------------
4121// Group
4122// -----------------------------------------------------------------------------
4123
4124/// A group of fields displayed together.
4125pub struct Group {
4126    fields: Vec<Box<dyn Field>>,
4127    current: usize,
4128    title: String,
4129    description: String,
4130    width: usize,
4131    #[allow(dead_code)]
4132    height: usize,
4133    theme: Option<Theme>,
4134    keymap: Option<KeyMap>,
4135    hide: Option<Box<dyn Fn() -> bool + Send + Sync>>,
4136}
4137
4138impl Default for Group {
4139    fn default() -> Self {
4140        Self::new(Vec::new())
4141    }
4142}
4143
4144impl Group {
4145    /// Creates a new group with the given fields.
4146    pub fn new(fields: Vec<Box<dyn Field>>) -> Self {
4147        Self {
4148            fields,
4149            current: 0,
4150            title: String::new(),
4151            description: String::new(),
4152            width: 80,
4153            height: 0,
4154            theme: None,
4155            keymap: None,
4156            hide: None,
4157        }
4158    }
4159
4160    /// Sets the group title.
4161    pub fn title(mut self, title: impl Into<String>) -> Self {
4162        self.title = title.into();
4163        self
4164    }
4165
4166    /// Sets the group description.
4167    pub fn description(mut self, description: impl Into<String>) -> Self {
4168        self.description = description.into();
4169        self
4170    }
4171
4172    /// Sets whether the group should be hidden.
4173    pub fn hide(mut self, hide: bool) -> Self {
4174        self.hide = Some(Box::new(move || hide));
4175        self
4176    }
4177
4178    /// Sets a function to determine if the group should be hidden.
4179    pub fn hide_func<F: Fn() -> bool + Send + Sync + 'static>(mut self, f: F) -> Self {
4180        self.hide = Some(Box::new(f));
4181        self
4182    }
4183
4184    /// Returns whether this group should be hidden.
4185    pub fn is_hidden(&self) -> bool {
4186        self.hide.as_ref().map(|f| f()).unwrap_or(false)
4187    }
4188
4189    /// Returns the current field index.
4190    pub fn current(&self) -> usize {
4191        self.current
4192    }
4193
4194    /// Returns the number of fields.
4195    pub fn len(&self) -> usize {
4196        self.fields.len()
4197    }
4198
4199    /// Returns whether the group has no fields.
4200    pub fn is_empty(&self) -> bool {
4201        self.fields.is_empty()
4202    }
4203
4204    /// Returns a reference to the current field.
4205    pub fn current_field(&self) -> Option<&dyn Field> {
4206        self.fields.get(self.current).map(|f| f.as_ref())
4207    }
4208
4209    /// Returns a mutable reference to the current field.
4210    pub fn current_field_mut(&mut self) -> Option<&mut Box<dyn Field>> {
4211        self.fields.get_mut(self.current)
4212    }
4213
4214    /// Collects all field errors.
4215    pub fn errors(&self) -> Vec<&str> {
4216        self.fields.iter().filter_map(|f| f.error()).collect()
4217    }
4218
4219    fn get_theme(&self) -> Theme {
4220        self.theme.clone().unwrap_or_else(theme_charm)
4221    }
4222
4223    /// Returns the header portion of the group (title and description).
4224    ///
4225    /// This is useful for custom layouts that want to render the header
4226    /// separately from the content.
4227    pub fn header(&self) -> String {
4228        let theme = self.get_theme();
4229        let mut output = String::new();
4230
4231        if !self.title.is_empty() {
4232            output.push_str(&theme.group.title.render(&self.title));
4233            output.push('\n');
4234        }
4235
4236        if !self.description.is_empty() {
4237            output.push_str(&theme.group.description.render(&self.description));
4238            output.push('\n');
4239        }
4240
4241        output
4242    }
4243
4244    /// Returns the content portion of the group (just the fields).
4245    ///
4246    /// This is useful for custom layouts that want to render the content
4247    /// separately from the header and footer.
4248    pub fn content(&self) -> String {
4249        let theme = self.get_theme();
4250        let mut output = String::new();
4251
4252        for (i, field) in self.fields.iter().enumerate() {
4253            output.push_str(&field.view());
4254            if i < self.fields.len() - 1 {
4255                output.push_str(&theme.field_separator.render(""));
4256            }
4257        }
4258
4259        output
4260    }
4261
4262    /// Returns the footer portion of the group (currently errors).
4263    ///
4264    /// This is useful for custom layouts that want to render the footer
4265    /// separately from the content.
4266    pub fn footer(&self) -> String {
4267        let theme = self.get_theme();
4268        let errors = self.errors();
4269
4270        if errors.is_empty() {
4271            return String::new();
4272        }
4273
4274        let error_text = errors.join(", ");
4275        theme.focused.error_message.render(&error_text)
4276    }
4277}
4278
4279impl Model for Group {
4280    fn init(&self) -> Option<Cmd> {
4281        None
4282    }
4283
4284    fn update(&mut self, msg: Message) -> Option<Cmd> {
4285        // Handle navigation messages
4286        if msg.is::<NextFieldMsg>() {
4287            if self.current < self.fields.len().saturating_sub(1) {
4288                if let Some(field) = self.fields.get_mut(self.current) {
4289                    field.blur();
4290                }
4291                self.current += 1;
4292                if let Some(field) = self.fields.get_mut(self.current) {
4293                    return field.focus();
4294                }
4295            } else {
4296                return Some(Cmd::new(|| Message::new(NextGroupMsg)));
4297            }
4298        } else if msg.is::<PrevFieldMsg>() {
4299            if self.current > 0 {
4300                if let Some(field) = self.fields.get_mut(self.current) {
4301                    field.blur();
4302                }
4303                self.current -= 1;
4304                if let Some(field) = self.fields.get_mut(self.current) {
4305                    return field.focus();
4306                }
4307            } else {
4308                return Some(Cmd::new(|| Message::new(PrevGroupMsg)));
4309            }
4310        }
4311
4312        // Forward to current field
4313        if let Some(field) = self.fields.get_mut(self.current) {
4314            return field.update(&msg);
4315        }
4316
4317        None
4318    }
4319
4320    fn view(&self) -> String {
4321        let theme = self.get_theme();
4322        let mut output = String::new();
4323
4324        // Title
4325        if !self.title.is_empty() {
4326            output.push_str(&theme.group.title.render(&self.title));
4327            output.push('\n');
4328        }
4329
4330        // Description
4331        if !self.description.is_empty() {
4332            output.push_str(&theme.group.description.render(&self.description));
4333            output.push('\n');
4334        }
4335
4336        // Fields
4337        for (i, field) in self.fields.iter().enumerate() {
4338            output.push_str(&field.view());
4339            if i < self.fields.len() - 1 {
4340                output.push_str(&theme.field_separator.render(""));
4341            }
4342        }
4343
4344        theme
4345            .group
4346            .base
4347            .width(self.width.try_into().unwrap_or(u16::MAX))
4348            .render(&output)
4349    }
4350}
4351
4352// -----------------------------------------------------------------------------
4353// Layout
4354// -----------------------------------------------------------------------------
4355
4356/// Layout determines how groups are arranged within a form.
4357///
4358/// The layout system controls how multiple groups are displayed:
4359/// - `Default`: Shows one group at a time (traditional wizard-style)
4360/// - `Stack`: Shows all groups stacked vertically
4361/// - `Columns`: Distributes groups across columns
4362/// - `Grid`: Arranges groups in a grid pattern
4363pub trait Layout: Send + Sync {
4364    /// Renders the form using this layout.
4365    fn view(&self, form: &Form) -> String;
4366
4367    /// Returns the width allocated to a specific group.
4368    fn group_width(&self, form: &Form, group_index: usize, total_width: usize) -> usize;
4369}
4370
4371/// Default layout - shows one group at a time.
4372///
4373/// This is the traditional wizard-style form layout where only the
4374/// current group is visible and users navigate between groups.
4375#[derive(Debug, Clone, Default)]
4376pub struct LayoutDefault;
4377
4378impl Layout for LayoutDefault {
4379    fn view(&self, form: &Form) -> String {
4380        if let Some(group) = form.groups.get(form.current_group) {
4381            if group.is_hidden() {
4382                return String::new();
4383            }
4384            form.theme
4385                .form
4386                .base
4387                .clone()
4388                .width(form.width.try_into().unwrap_or(u16::MAX))
4389                .render(&group.view())
4390        } else {
4391            String::new()
4392        }
4393    }
4394
4395    fn group_width(&self, form: &Form, _group_index: usize, _total_width: usize) -> usize {
4396        form.width
4397    }
4398}
4399
4400/// Stack layout - shows all groups stacked vertically.
4401///
4402/// All groups are rendered one after another, with the form's
4403/// field separator between them.
4404#[derive(Debug, Clone, Default)]
4405pub struct LayoutStack;
4406
4407impl Layout for LayoutStack {
4408    fn view(&self, form: &Form) -> String {
4409        let mut output = String::new();
4410        let visible_groups: Vec<_> = form
4411            .groups
4412            .iter()
4413            .enumerate()
4414            .filter(|(_, g)| !g.is_hidden())
4415            .collect();
4416
4417        for (i, (_, group)) in visible_groups.iter().enumerate() {
4418            output.push_str(&group.view());
4419            if i < visible_groups.len() - 1 {
4420                output.push('\n');
4421            }
4422        }
4423
4424        form.theme
4425            .form
4426            .base
4427            .clone()
4428            .width(form.width.try_into().unwrap_or(u16::MAX))
4429            .render(&output)
4430    }
4431
4432    fn group_width(&self, form: &Form, _group_index: usize, _total_width: usize) -> usize {
4433        form.width
4434    }
4435}
4436
4437/// Columns layout - distributes groups across columns.
4438///
4439/// Groups are arranged in columns, wrapping to the next row when needed.
4440#[derive(Debug, Clone)]
4441pub struct LayoutColumns {
4442    columns: usize,
4443}
4444
4445impl LayoutColumns {
4446    /// Creates a new columns layout with the specified number of columns.
4447    pub fn new(columns: usize) -> Self {
4448        Self {
4449            columns: columns.max(1),
4450        }
4451    }
4452}
4453
4454impl Default for LayoutColumns {
4455    fn default() -> Self {
4456        Self::new(2)
4457    }
4458}
4459
4460impl Layout for LayoutColumns {
4461    fn view(&self, form: &Form) -> String {
4462        let visible_groups: Vec<_> = form
4463            .groups
4464            .iter()
4465            .enumerate()
4466            .filter(|(_, g)| !g.is_hidden())
4467            .collect();
4468
4469        if visible_groups.is_empty() {
4470            return String::new();
4471        }
4472
4473        let column_width = form.width / self.columns;
4474        let mut rows: Vec<String> = Vec::new();
4475
4476        for chunk in visible_groups.chunks(self.columns) {
4477            let mut row_parts: Vec<String> = Vec::new();
4478            for (_, group) in chunk {
4479                // Render each group with column width
4480                let group_view = group.view();
4481                // Pad to column width
4482                let lines: Vec<&str> = group_view.lines().collect();
4483                let padded: Vec<String> = lines
4484                    .iter()
4485                    .map(|line| {
4486                        let visual_width = lipgloss::width(line);
4487                        if visual_width < column_width {
4488                            format!("{}{}", line, " ".repeat(column_width - visual_width))
4489                        } else {
4490                            line.to_string()
4491                        }
4492                    })
4493                    .collect();
4494                row_parts.push(padded.join("\n"));
4495            }
4496
4497            // Join columns horizontally using lipgloss
4498            if row_parts.len() == 1 {
4499                // Keep render path panic-free even if future refactors alter row_parts population.
4500                rows.push(row_parts.into_iter().next().unwrap_or_default());
4501            } else {
4502                let row_refs: Vec<&str> = row_parts.iter().map(|s| s.as_str()).collect();
4503                rows.push(lipgloss::join_horizontal(
4504                    lipgloss::Position::Top,
4505                    &row_refs,
4506                ));
4507            }
4508        }
4509
4510        let output = rows.join("\n");
4511        form.theme
4512            .form
4513            .base
4514            .clone()
4515            .width(form.width.try_into().unwrap_or(u16::MAX))
4516            .render(&output)
4517    }
4518
4519    fn group_width(&self, form: &Form, _group_index: usize, _total_width: usize) -> usize {
4520        form.width / self.columns
4521    }
4522}
4523
4524/// Grid layout - arranges groups in a fixed grid pattern.
4525///
4526/// Groups are arranged in a grid with the specified number of rows and columns.
4527/// If there are more groups than cells, extra groups are not displayed.
4528#[derive(Debug, Clone)]
4529pub struct LayoutGrid {
4530    rows: usize,
4531    columns: usize,
4532}
4533
4534impl LayoutGrid {
4535    /// Creates a new grid layout with the specified dimensions.
4536    pub fn new(rows: usize, columns: usize) -> Self {
4537        Self {
4538            rows: rows.max(1),
4539            columns: columns.max(1),
4540        }
4541    }
4542}
4543
4544impl Default for LayoutGrid {
4545    fn default() -> Self {
4546        Self::new(2, 2)
4547    }
4548}
4549
4550impl Layout for LayoutGrid {
4551    fn view(&self, form: &Form) -> String {
4552        let visible_groups: Vec<_> = form
4553            .groups
4554            .iter()
4555            .enumerate()
4556            .filter(|(_, g)| !g.is_hidden())
4557            .collect();
4558
4559        if visible_groups.is_empty() {
4560            return String::new();
4561        }
4562
4563        let column_width = form.width / self.columns;
4564        let max_cells = self.rows * self.columns;
4565        let mut rows: Vec<String> = Vec::new();
4566
4567        for row_idx in 0..self.rows {
4568            let start = row_idx * self.columns;
4569            if start >= visible_groups.len() || start >= max_cells {
4570                break;
4571            }
4572            let end = (start + self.columns)
4573                .min(visible_groups.len())
4574                .min(max_cells);
4575
4576            let mut row_parts: Vec<String> = Vec::new();
4577            for (_, group) in &visible_groups[start..end] {
4578                let group_view = group.view();
4579                let lines: Vec<&str> = group_view.lines().collect();
4580                let padded: Vec<String> = lines
4581                    .iter()
4582                    .map(|line| {
4583                        let visual_width = lipgloss::width(line);
4584                        if visual_width < column_width {
4585                            format!("{}{}", line, " ".repeat(column_width - visual_width))
4586                        } else {
4587                            line.to_string()
4588                        }
4589                    })
4590                    .collect();
4591                row_parts.push(padded.join("\n"));
4592            }
4593
4594            if row_parts.len() == 1 {
4595                // Keep render path panic-free even if future refactors alter row_parts population.
4596                rows.push(row_parts.into_iter().next().unwrap_or_default());
4597            } else {
4598                let row_refs: Vec<&str> = row_parts.iter().map(|s| s.as_str()).collect();
4599                rows.push(lipgloss::join_horizontal(
4600                    lipgloss::Position::Top,
4601                    &row_refs,
4602                ));
4603            }
4604        }
4605
4606        let output = rows.join("\n");
4607        form.theme
4608            .form
4609            .base
4610            .clone()
4611            .width(form.width.try_into().unwrap_or(u16::MAX))
4612            .render(&output)
4613    }
4614
4615    fn group_width(&self, form: &Form, _group_index: usize, _total_width: usize) -> usize {
4616        form.width / self.columns
4617    }
4618}
4619
4620// -----------------------------------------------------------------------------
4621// Form
4622// -----------------------------------------------------------------------------
4623
4624/// A form containing multiple groups of fields.
4625pub struct Form {
4626    groups: Vec<Group>,
4627    current_group: usize,
4628    state: FormState,
4629    width: usize,
4630    theme: Theme,
4631    keymap: KeyMap,
4632    layout: Box<dyn Layout>,
4633    show_help: bool,
4634    show_errors: bool,
4635    accessible: bool,
4636}
4637
4638impl Default for Form {
4639    fn default() -> Self {
4640        Self::new(Vec::new())
4641    }
4642}
4643
4644impl Form {
4645    /// Creates a new form with the given groups.
4646    pub fn new(groups: Vec<Group>) -> Self {
4647        Self {
4648            groups,
4649            current_group: 0,
4650            state: FormState::Normal,
4651            width: 80,
4652            theme: theme_charm(),
4653            keymap: KeyMap::default(),
4654            layout: Box::new(LayoutDefault),
4655            show_help: true,
4656            show_errors: true,
4657            accessible: false,
4658        }
4659    }
4660
4661    /// Sets the form width.
4662    pub fn width(mut self, width: usize) -> Self {
4663        self.width = width;
4664        self
4665    }
4666
4667    /// Sets the theme.
4668    pub fn theme(mut self, theme: Theme) -> Self {
4669        self.theme = theme;
4670        self
4671    }
4672
4673    /// Sets the keymap.
4674    pub fn keymap(mut self, keymap: KeyMap) -> Self {
4675        self.keymap = keymap;
4676        self
4677    }
4678
4679    /// Sets the layout for the form.
4680    ///
4681    /// # Example
4682    ///
4683    /// ```rust,ignore
4684    /// use huh::{Form, Group, LayoutColumns};
4685    ///
4686    /// let form = Form::new(vec![group1, group2, group3])
4687    ///     .layout(LayoutColumns::new(2));
4688    /// ```
4689    pub fn layout<L: Layout + 'static>(mut self, layout: L) -> Self {
4690        self.layout = Box::new(layout);
4691        self
4692    }
4693
4694    /// Sets whether to show help at the bottom of the form.
4695    pub fn show_help(mut self, show: bool) -> Self {
4696        self.show_help = show;
4697        self
4698    }
4699
4700    /// Sets whether to show validation errors.
4701    pub fn show_errors(mut self, show: bool) -> Self {
4702        self.show_errors = show;
4703        self
4704    }
4705
4706    /// Enables or disables accessible mode.
4707    ///
4708    /// When accessible mode is enabled, the form renders in a more
4709    /// screen-reader-friendly format with simpler styling and clearer
4710    /// field labels. This mode prioritizes accessibility over visual
4711    /// aesthetics.
4712    ///
4713    /// # Example
4714    ///
4715    /// ```rust,ignore
4716    /// use huh::Form;
4717    ///
4718    /// let form = Form::new(groups)
4719    ///     .with_accessible(true);
4720    /// ```
4721    pub fn with_accessible(mut self, accessible: bool) -> Self {
4722        self.accessible = accessible;
4723        self
4724    }
4725
4726    /// Returns whether accessible mode is enabled.
4727    pub fn is_accessible(&self) -> bool {
4728        self.accessible
4729    }
4730
4731    /// Returns the form state.
4732    pub fn state(&self) -> FormState {
4733        self.state
4734    }
4735
4736    /// Returns the current group index.
4737    pub fn current_group(&self) -> usize {
4738        self.current_group
4739    }
4740
4741    /// Returns the number of groups.
4742    pub fn len(&self) -> usize {
4743        self.groups.len()
4744    }
4745
4746    /// Returns whether the form has no groups.
4747    pub fn is_empty(&self) -> bool {
4748        self.groups.is_empty()
4749    }
4750
4751    /// Initializes all fields with theme and keymap.
4752    fn init_fields(&mut self) {
4753        for group in &mut self.groups {
4754            group.theme = Some(self.theme.clone());
4755            group.keymap = Some(self.keymap.clone());
4756            group.width = self.width;
4757            for field in &mut group.fields {
4758                field.with_theme(&self.theme);
4759                field.with_keymap(&self.keymap);
4760                field.with_width(self.width);
4761            }
4762        }
4763    }
4764
4765    fn next_group(&mut self) -> Option<Cmd> {
4766        // Skip hidden groups
4767        loop {
4768            if self.current_group >= self.groups.len().saturating_sub(1) {
4769                self.state = FormState::Completed;
4770                return Some(bubbletea::quit());
4771            }
4772            self.current_group += 1;
4773            if !self.groups[self.current_group].is_hidden() {
4774                break;
4775            }
4776        }
4777        // Focus first field of new group
4778        if let Some(group) = self.groups.get_mut(self.current_group) {
4779            group.current = 0;
4780            if let Some(field) = group.fields.get_mut(0) {
4781                return field.focus();
4782            }
4783        }
4784        None
4785    }
4786
4787    fn prev_group(&mut self) -> Option<Cmd> {
4788        // Skip hidden groups
4789        loop {
4790            if self.current_group == 0 {
4791                return None;
4792            }
4793            self.current_group -= 1;
4794            if !self.groups[self.current_group].is_hidden() {
4795                break;
4796            }
4797        }
4798        // Focus last field of new group
4799        if let Some(group) = self.groups.get_mut(self.current_group) {
4800            group.current = group.fields.len().saturating_sub(1);
4801            if let Some(field) = group.fields.last_mut() {
4802                return field.focus();
4803            }
4804        }
4805        None
4806    }
4807
4808    /// Returns the value of a field by key.
4809    pub fn get_value(&self, key: &str) -> Option<Box<dyn Any>> {
4810        for group in &self.groups {
4811            for field in &group.fields {
4812                if field.get_key() == key {
4813                    return Some(field.get_value());
4814                }
4815            }
4816        }
4817        None
4818    }
4819
4820    /// Returns the string value of a field by key.
4821    pub fn get_string(&self, key: &str) -> Option<String> {
4822        self.get_value(key)
4823            .and_then(|v| v.downcast::<String>().ok())
4824            .map(|v| *v)
4825    }
4826
4827    /// Returns the boolean value of a field by key.
4828    pub fn get_bool(&self, key: &str) -> Option<bool> {
4829        self.get_value(key)
4830            .and_then(|v| v.downcast::<bool>().ok())
4831            .map(|v| *v)
4832    }
4833
4834    /// Collects all validation errors from all groups.
4835    pub fn all_errors(&self) -> Vec<String> {
4836        self.groups
4837            .iter()
4838            .flat_map(|g| g.errors())
4839            .map(|s| s.to_string())
4840            .collect()
4841    }
4842
4843    /// Returns a view of all validation errors.
4844    fn errors_view(&self) -> String {
4845        let errors = self.all_errors();
4846        if errors.is_empty() {
4847            return String::new();
4848        }
4849
4850        let error_text = errors.join(", ");
4851        self.theme.focused.error_message.render(&error_text)
4852    }
4853
4854    /// Returns a help view with available keybindings.
4855    fn help_view(&self) -> String {
4856        // Build help text from keybindings
4857        let mut help_parts = Vec::new();
4858
4859        // Get current field's keybindings if available
4860        if let Some(group) = self.groups.get(self.current_group)
4861            && let Some(field) = group.fields.get(group.current)
4862        {
4863            for binding in field.key_binds() {
4864                let help = binding.get_help();
4865                if binding.enabled() && !help.desc.is_empty() {
4866                    let keys = binding.get_keys();
4867                    if !keys.is_empty() {
4868                        help_parts.push(format!("{}: {}", keys.join("/"), help.desc));
4869                    }
4870                }
4871            }
4872        }
4873
4874        // Add form-level keybindings
4875        let quit_help = self.keymap.quit.get_help();
4876        if self.keymap.quit.enabled() && !quit_help.desc.is_empty() {
4877            let keys = self.keymap.quit.get_keys();
4878            if !keys.is_empty() {
4879                help_parts.push(format!("{}: {}", keys.join("/"), quit_help.desc));
4880            }
4881        }
4882
4883        if help_parts.is_empty() {
4884            return String::new();
4885        }
4886
4887        // Style the help text
4888        let help_text = help_parts.join(" • ");
4889        self.theme.help.render(&help_text)
4890    }
4891
4892    /// Returns the width allocated to a specific group based on the current layout.
4893    pub fn group_width(&self, group_index: usize) -> usize {
4894        self.layout.group_width(self, group_index, self.width)
4895    }
4896}
4897
4898impl Model for Form {
4899    fn init(&self) -> Option<Cmd> {
4900        None
4901    }
4902
4903    fn update(&mut self, msg: Message) -> Option<Cmd> {
4904        // Initialize fields on first update
4905        if self.state == FormState::Normal && self.current_group == 0 {
4906            self.init_fields();
4907            // Focus first field
4908            if let Some(group) = self.groups.get_mut(0)
4909                && let Some(field) = group.fields.get_mut(0)
4910            {
4911                field.focus();
4912            }
4913        }
4914
4915        // Handle quit
4916        if let Some(key_msg) = msg.downcast_ref::<KeyMsg>()
4917            && binding_matches(&self.keymap.quit, key_msg)
4918        {
4919            self.state = FormState::Aborted;
4920            return Some(bubbletea::quit());
4921        }
4922
4923        // Handle group navigation
4924        if msg.is::<NextGroupMsg>() {
4925            return self.next_group();
4926        } else if msg.is::<PrevGroupMsg>() {
4927            return self.prev_group();
4928        }
4929
4930        // Forward to current group
4931        if let Some(group) = self.groups.get_mut(self.current_group) {
4932            return group.update(msg);
4933        }
4934
4935        None
4936    }
4937
4938    fn view(&self) -> String {
4939        let mut output = self.layout.view(self);
4940
4941        // Add help footer if enabled
4942        if self.show_help {
4943            let help_text = self.help_view();
4944            if !help_text.is_empty() {
4945                output.push('\n');
4946                output.push_str(&help_text);
4947            }
4948        }
4949
4950        // Add errors if enabled
4951        if self.show_errors {
4952            let errors = self.errors_view();
4953            if !errors.is_empty() {
4954                output.push('\n');
4955                output.push_str(&errors);
4956            }
4957        }
4958
4959        output
4960    }
4961}
4962
4963// -----------------------------------------------------------------------------
4964// Validators
4965// -----------------------------------------------------------------------------
4966
4967/// Creates a validator that checks if the input is not empty.
4968///
4969/// **Note**: Due to Rust function pointer limitations, the `_field_name` parameter
4970/// is not used. It exists only for API compatibility. To create validators with
4971/// custom error messages, use a closure directly:
4972///
4973/// ```rust,ignore
4974/// let validator = |s: &str| {
4975///     if s.trim().is_empty() {
4976///         Some("username is required".to_string())
4977///     } else {
4978///         None
4979///     }
4980/// };
4981/// ```
4982///
4983/// # Example
4984/// ```
4985/// use huh::validate_required;
4986/// let validator = validate_required("any");
4987/// assert!(validator("").is_some()); // Error: "field is required"
4988/// assert!(validator("John").is_none()); // Valid
4989/// ```
4990pub fn validate_required(_field_name: &'static str) -> fn(&str) -> Option<String> {
4991    |s| {
4992        if s.trim().is_empty() {
4993            Some("field is required".to_string())
4994        } else {
4995            None
4996        }
4997    }
4998}
4999
5000/// Creates a required validator for the "name" field.
5001pub fn validate_required_name() -> fn(&str) -> Option<String> {
5002    |s| {
5003        if s.trim().is_empty() {
5004            Some("name is required".to_string())
5005        } else {
5006            None
5007        }
5008    }
5009}
5010
5011/// Creates a min length validator for password fields.
5012/// Note: Due to Rust's function pointer limitations, this returns a closure
5013/// that can be converted to a function pointer.
5014pub fn validate_min_length_8() -> fn(&str) -> Option<String> {
5015    |s| {
5016        if s.chars().count() < 8 {
5017            Some("password must be at least 8 characters".to_string())
5018        } else {
5019            None
5020        }
5021    }
5022}
5023
5024/// Creates a validator for email format.
5025/// Uses a simple regex pattern to validate email addresses.
5026pub fn validate_email() -> fn(&str) -> Option<String> {
5027    |s| {
5028        if s.is_empty() {
5029            return Some("email is required".to_string());
5030        }
5031        // Simple email validation: must have @ with something before and after
5032        // and a dot after the @
5033        let parts: Vec<&str> = s.split('@').collect();
5034        if parts.len() != 2 {
5035            return Some("invalid email address".to_string());
5036        }
5037        let (local, domain) = (parts[0], parts[1]);
5038        if local.is_empty() || domain.is_empty() || !domain.contains('.') {
5039            return Some("invalid email address".to_string());
5040        }
5041        // Check domain has something after the dot
5042        let domain_parts: Vec<&str> = domain.split('.').collect();
5043        if domain_parts.len() < 2 || domain_parts.iter().any(|p| p.is_empty()) {
5044            return Some("invalid email address".to_string());
5045        }
5046        None
5047    }
5048}
5049
5050// -----------------------------------------------------------------------------
5051// Tests
5052// -----------------------------------------------------------------------------
5053
5054#[cfg(test)]
5055mod tests {
5056    use super::*;
5057
5058    #[test]
5059    fn test_form_error_display() {
5060        let err = FormError::UserAborted;
5061        assert_eq!(format!("{}", err), "user aborted");
5062
5063        let err = FormError::Validation("invalid input".to_string());
5064        assert_eq!(format!("{}", err), "validation error: invalid input");
5065    }
5066
5067    #[test]
5068    fn test_form_state_default() {
5069        let state = FormState::default();
5070        assert_eq!(state, FormState::Normal);
5071    }
5072
5073    #[test]
5074    fn test_select_option() {
5075        let opt = SelectOption::new("Red", "red".to_string());
5076        assert_eq!(opt.key, "Red");
5077        assert_eq!(opt.value, "red");
5078        assert!(!opt.selected);
5079
5080        let opt = opt.selected(true);
5081        assert!(opt.selected);
5082    }
5083
5084    #[test]
5085    fn test_new_options() {
5086        let opts = new_options(["apple", "banana", "cherry"]);
5087        assert_eq!(opts.len(), 3);
5088        assert_eq!(opts[0].key, "apple");
5089        assert_eq!(opts[0].value, "apple");
5090    }
5091
5092    #[test]
5093    fn test_input_builder() {
5094        let input = Input::new()
5095            .key("name")
5096            .title("Name")
5097            .description("Enter your name")
5098            .placeholder("John Doe")
5099            .value("Jane");
5100
5101        assert_eq!(input.get_key(), "name");
5102        assert_eq!(input.get_string_value(), "Jane");
5103    }
5104
5105    #[test]
5106    fn test_confirm_builder() {
5107        let confirm = Confirm::new()
5108            .key("agree")
5109            .title("Terms")
5110            .affirmative("I Agree")
5111            .negative("I Disagree")
5112            .value(true);
5113
5114        assert_eq!(confirm.get_key(), "agree");
5115        assert!(confirm.get_bool_value());
5116    }
5117
5118    #[test]
5119    fn test_note_builder() {
5120        let note = Note::new()
5121            .key("info")
5122            .title("Information")
5123            .description("This is an informational note.");
5124
5125        assert_eq!(note.get_key(), "info");
5126    }
5127
5128    #[test]
5129    fn test_text_builder() {
5130        let text = Text::new()
5131            .key("bio")
5132            .title("Biography")
5133            .description("Tell us about yourself")
5134            .placeholder("Enter your bio...")
5135            .lines(10)
5136            .value("Hello world");
5137
5138        assert_eq!(text.get_key(), "bio");
5139        assert_eq!(text.get_string_value(), "Hello world");
5140    }
5141
5142    #[test]
5143    fn test_text_char_limit() {
5144        let text = Text::new().char_limit(50).show_line_numbers(true);
5145
5146        assert_eq!(text.char_limit, 50);
5147        assert!(text.show_line_numbers);
5148    }
5149
5150    #[test]
5151    fn test_filepicker_builder() {
5152        let picker = FilePicker::new()
5153            .key("config_file")
5154            .title("Select Configuration")
5155            .description("Choose a file")
5156            .current_directory("/tmp")
5157            .show_hidden(true)
5158            .file_allowed(true)
5159            .dir_allowed(false);
5160
5161        assert_eq!(picker.get_key(), "config_file");
5162        assert!(picker.file_allowed);
5163        assert!(!picker.dir_allowed);
5164        assert!(picker.show_hidden);
5165    }
5166
5167    #[test]
5168    fn test_filepicker_allowed_types() {
5169        let picker = FilePicker::new()
5170            .allowed_types(vec![".toml".to_string(), ".json".to_string()])
5171            .show_size(true);
5172
5173        assert_eq!(picker.allowed_types.len(), 2);
5174        assert!(picker.show_size);
5175    }
5176
5177    #[test]
5178    fn test_select_builder() {
5179        let select: Select<String> =
5180            Select::new()
5181                .key("color")
5182                .title("Favorite Color")
5183                .options(vec![
5184                    SelectOption::new("Red", "red".to_string()),
5185                    SelectOption::new("Green", "green".to_string()).selected(true),
5186                    SelectOption::new("Blue", "blue".to_string()),
5187                ]);
5188
5189        assert_eq!(select.get_key(), "color");
5190        assert_eq!(select.get_selected_value(), Some(&"green".to_string()));
5191    }
5192
5193    #[test]
5194    fn test_theme_base() {
5195        let theme = theme_base();
5196        assert!(!theme.focused.title.value().is_empty() || theme.focused.title.value().is_empty());
5197    }
5198
5199    #[test]
5200    fn test_theme_charm() {
5201        let theme = theme_charm();
5202        // Just verify it doesn't panic
5203        let _ = theme.focused.title.render("Test");
5204    }
5205
5206    #[test]
5207    fn test_theme_dracula() {
5208        let theme = theme_dracula();
5209        let _ = theme.focused.title.render("Test");
5210    }
5211
5212    #[test]
5213    fn test_theme_base16() {
5214        let theme = theme_base16();
5215        let _ = theme.focused.title.render("Test");
5216    }
5217
5218    #[test]
5219    fn test_theme_catppuccin() {
5220        let theme = theme_catppuccin();
5221        // Verify it doesn't panic and has expected Catppuccin colors
5222        let _ = theme.focused.title.render("Test");
5223        let _ = theme.focused.selected_option.render("Selected");
5224        let _ = theme.focused.focused_button.render("OK");
5225        let _ = theme.blurred.title.render("Blurred");
5226    }
5227
5228    #[test]
5229    fn test_keymap_default() {
5230        let keymap = KeyMap::default();
5231        assert!(keymap.quit.enabled());
5232        assert!(keymap.input.next.enabled());
5233    }
5234
5235    #[test]
5236    fn test_field_position() {
5237        let pos = FieldPosition {
5238            group: 0,
5239            field: 0,
5240            first_field: 0,
5241            last_field: 2,
5242            group_count: 2,
5243            first_group: 0,
5244            last_group: 1,
5245        };
5246        assert!(pos.is_first());
5247        assert!(!pos.is_last());
5248    }
5249
5250    #[test]
5251    fn test_group_basic() {
5252        let group = Group::new(vec![
5253            Box::new(Input::new().key("name").title("Name")),
5254            Box::new(Input::new().key("email").title("Email")),
5255        ]);
5256
5257        assert_eq!(group.len(), 2);
5258        assert!(!group.is_empty());
5259        assert_eq!(group.current(), 0);
5260    }
5261
5262    #[test]
5263    fn test_group_hide() {
5264        let group = Group::new(Vec::new()).hide(true);
5265        assert!(group.is_hidden());
5266
5267        let group = Group::new(Vec::new()).hide(false);
5268        assert!(!group.is_hidden());
5269    }
5270
5271    #[test]
5272    fn test_form_basic() {
5273        let form = Form::new(vec![Group::new(vec![Box::new(Input::new().key("name"))])]);
5274
5275        assert_eq!(form.len(), 1);
5276        assert!(!form.is_empty());
5277        assert_eq!(form.state(), FormState::Normal);
5278    }
5279
5280    #[test]
5281    fn test_input_echo_mode() {
5282        let input = Input::new().password(true);
5283        assert_eq!(input.echo_mode, EchoMode::Password);
5284
5285        let input = Input::new().echo_mode(EchoMode::None);
5286        assert_eq!(input.echo_mode, EchoMode::None);
5287    }
5288
5289    #[test]
5290    fn test_key_to_string() {
5291        let key = KeyMsg {
5292            key_type: KeyType::Enter,
5293            runes: vec![],
5294            alt: false,
5295            paste: false,
5296        };
5297        assert_eq!(key.to_string(), "enter");
5298
5299        let key = KeyMsg {
5300            key_type: KeyType::Runes,
5301            runes: vec!['a'],
5302            alt: false,
5303            paste: false,
5304        };
5305        assert_eq!(key.to_string(), "a");
5306
5307        let key = KeyMsg {
5308            key_type: KeyType::CtrlC,
5309            runes: vec![],
5310            alt: false,
5311            paste: false,
5312        };
5313        assert_eq!(key.to_string(), "ctrl+c");
5314    }
5315
5316    #[test]
5317    fn test_input_view() {
5318        let input = Input::new()
5319            .title("Name")
5320            .placeholder("Enter name")
5321            .value("");
5322
5323        let view = input.view();
5324        assert!(view.contains("Name"));
5325    }
5326
5327    #[test]
5328    fn test_confirm_view() {
5329        let confirm = Confirm::new()
5330            .title("Proceed?")
5331            .affirmative("Yes")
5332            .negative("No");
5333
5334        let view = confirm.view();
5335        assert!(view.contains("Proceed"));
5336    }
5337
5338    #[test]
5339    fn test_select_view() {
5340        let select: Select<String> = Select::new().title("Choose").options(vec![
5341            SelectOption::new("A", "a".to_string()),
5342            SelectOption::new("B", "b".to_string()),
5343        ]);
5344
5345        let view = select.view();
5346        assert!(view.contains("Choose"));
5347    }
5348
5349    #[test]
5350    fn test_note_view() {
5351        let note = Note::new().title("Info").description("Some information");
5352
5353        let view = note.view();
5354        assert!(view.contains("Info"));
5355    }
5356
5357    #[test]
5358    fn test_multiselect_view() {
5359        let multi: MultiSelect<String> = MultiSelect::new().title("Select items").options(vec![
5360            SelectOption::new("A", "a".to_string()),
5361            SelectOption::new("B", "b".to_string()).selected(true),
5362            SelectOption::new("C", "c".to_string()),
5363        ]);
5364
5365        let view = multi.view();
5366        assert!(view.contains("Select items"));
5367    }
5368
5369    #[test]
5370    fn test_multiselect_initial_selection() {
5371        let multi: MultiSelect<String> = MultiSelect::new().options(vec![
5372            SelectOption::new("A", "a".to_string()),
5373            SelectOption::new("B", "b".to_string()).selected(true),
5374            SelectOption::new("C", "c".to_string()).selected(true),
5375        ]);
5376
5377        let selected = multi.get_selected_values();
5378        assert_eq!(selected.len(), 2);
5379        assert!(selected.contains(&&"b".to_string()));
5380        assert!(selected.contains(&&"c".to_string()));
5381    }
5382
5383    #[test]
5384    fn test_multiselect_limit() {
5385        let mut multi: MultiSelect<String> = MultiSelect::new().limit(2).options(vec![
5386            SelectOption::new("A", "a".to_string()),
5387            SelectOption::new("B", "b".to_string()),
5388            SelectOption::new("C", "c".to_string()),
5389        ]);
5390
5391        // Focus the field so it processes updates
5392        multi.focus();
5393
5394        // Toggle first option (select)
5395        let toggle_msg = Message::new(KeyMsg {
5396            key_type: KeyType::Runes,
5397            runes: vec![' '],
5398            alt: false,
5399            paste: false,
5400        });
5401        multi.update(&toggle_msg);
5402        assert_eq!(multi.get_selected_values().len(), 1);
5403
5404        // Move down and toggle second
5405        let down_msg = Message::new(KeyMsg {
5406            key_type: KeyType::Down,
5407            runes: vec![],
5408            alt: false,
5409            paste: false,
5410        });
5411        multi.update(&down_msg);
5412        multi.update(&toggle_msg);
5413        assert_eq!(multi.get_selected_values().len(), 2);
5414
5415        // Move down and try to toggle third (should be blocked by limit)
5416        multi.update(&down_msg);
5417        multi.update(&toggle_msg);
5418        // Should still be 2 due to limit
5419        assert_eq!(multi.get_selected_values().len(), 2);
5420    }
5421
5422    #[test]
5423    fn test_input_unicode_cursor_handling() {
5424        // Test that cursor position works correctly with multi-byte UTF-8 characters
5425        let mut input = Input::new().value("café"); // 'é' is 2 bytes in UTF-8
5426
5427        // Focus to enable updates
5428        input.focus();
5429
5430        // cursor_pos should be at end (4 characters, not 5 bytes)
5431        assert_eq!(input.cursor_pos, 4);
5432        assert_eq!(input.value.chars().count(), 4);
5433
5434        // Press End to ensure cursor is at end
5435        let end_msg = Message::new(KeyMsg {
5436            key_type: KeyType::End,
5437            runes: vec![],
5438            alt: false,
5439            paste: false,
5440        });
5441        input.update(&end_msg);
5442        assert_eq!(input.cursor_pos, 4);
5443
5444        // Press Left to move before 'é'
5445        let left_msg = Message::new(KeyMsg {
5446            key_type: KeyType::Left,
5447            runes: vec![],
5448            alt: false,
5449            paste: false,
5450        });
5451        input.update(&left_msg);
5452        assert_eq!(input.cursor_pos, 3);
5453
5454        // Press Backspace to delete 'f'
5455        let backspace_msg = Message::new(KeyMsg {
5456            key_type: KeyType::Backspace,
5457            runes: vec![],
5458            alt: false,
5459            paste: false,
5460        });
5461        input.update(&backspace_msg);
5462        assert_eq!(input.get_string_value(), "caé");
5463        assert_eq!(input.cursor_pos, 2);
5464
5465        // Insert a character at current position
5466        let insert_msg = Message::new(KeyMsg {
5467            key_type: KeyType::Runes,
5468            runes: vec!['ñ'], // Another multi-byte char
5469            alt: false,
5470            paste: false,
5471        });
5472        input.update(&insert_msg);
5473        assert_eq!(input.get_string_value(), "cañé");
5474        assert_eq!(input.cursor_pos, 3);
5475
5476        // Delete character at cursor (should delete 'é')
5477        let delete_msg = Message::new(KeyMsg {
5478            key_type: KeyType::Delete,
5479            runes: vec![],
5480            alt: false,
5481            paste: false,
5482        });
5483        input.update(&delete_msg);
5484        assert_eq!(input.get_string_value(), "cañ");
5485
5486        // Home should move to position 0
5487        let home_msg = Message::new(KeyMsg {
5488            key_type: KeyType::Home,
5489            runes: vec![],
5490            alt: false,
5491            paste: false,
5492        });
5493        input.update(&home_msg);
5494        assert_eq!(input.cursor_pos, 0);
5495    }
5496
5497    #[test]
5498    fn test_input_char_limit_with_unicode() {
5499        // Test that char_limit counts characters, not bytes
5500        let mut input = Input::new().char_limit(5);
5501        input.focus();
5502
5503        // Insert 5 multi-byte characters (each would be 2+ bytes in UTF-8)
5504        let chars = ['日', '本', '語', '文', '字']; // 5 Japanese characters
5505        for c in chars {
5506            let msg = Message::new(KeyMsg {
5507                key_type: KeyType::Runes,
5508                runes: vec![c],
5509                alt: false,
5510                paste: false,
5511            });
5512            input.update(&msg);
5513        }
5514
5515        // Should have exactly 5 characters (not blocked due to byte count)
5516        assert_eq!(input.value.chars().count(), 5);
5517        assert_eq!(input.get_string_value(), "日本語文字");
5518
5519        // Try to add one more - should be blocked by char limit
5520        let msg = Message::new(KeyMsg {
5521            key_type: KeyType::Runes,
5522            runes: vec!['!'],
5523            alt: false,
5524            paste: false,
5525        });
5526        input.update(&msg);
5527
5528        // Should still be 5 characters
5529        assert_eq!(input.value.chars().count(), 5);
5530    }
5531
5532    #[test]
5533    fn test_layout_default() {
5534        let _layout = LayoutDefault;
5535        // Just ensure it compiles and can be created
5536    }
5537
5538    #[test]
5539    fn test_layout_stack() {
5540        let _layout = LayoutStack;
5541        // Just ensure it compiles and can be created
5542    }
5543
5544    #[test]
5545    fn test_layout_columns() {
5546        let layout = LayoutColumns::new(3);
5547        assert_eq!(layout.columns, 3);
5548
5549        // Minimum of 1 column
5550        let layout = LayoutColumns::new(0);
5551        assert_eq!(layout.columns, 1);
5552    }
5553
5554    #[test]
5555    fn test_layout_grid() {
5556        let layout = LayoutGrid::new(2, 3);
5557        assert_eq!(layout.rows, 2);
5558        assert_eq!(layout.columns, 3);
5559
5560        // Minimum of 1x1
5561        let layout = LayoutGrid::new(0, 0);
5562        assert_eq!(layout.rows, 1);
5563        assert_eq!(layout.columns, 1);
5564    }
5565
5566    #[test]
5567    fn test_layout_columns_view_single_empty_group_no_panic() {
5568        let form = Form::new(vec![Group::new(Vec::new())]).layout(LayoutColumns::new(1));
5569        let _ = form.view();
5570    }
5571
5572    #[test]
5573    fn test_layout_grid_view_single_empty_group_no_panic() {
5574        let form = Form::new(vec![Group::new(Vec::new())]).layout(LayoutGrid::new(1, 1));
5575        let _ = form.view();
5576    }
5577
5578    #[test]
5579    fn test_form_with_layout() {
5580        let form = Form::new(vec![
5581            Group::new(vec![Box::new(Input::new().key("a"))]),
5582            Group::new(vec![Box::new(Input::new().key("b"))]),
5583        ])
5584        .layout(LayoutColumns::new(2));
5585
5586        // Form should have the layout set
5587        assert_eq!(form.len(), 2);
5588    }
5589
5590    #[test]
5591    fn test_form_show_help() {
5592        let form = Form::new(Vec::new()).show_help(false).show_errors(false);
5593
5594        // Just verify the builder works
5595        assert!(!form.show_help);
5596        assert!(!form.show_errors);
5597    }
5598
5599    #[test]
5600    fn test_group_header_footer_content() {
5601        let group = Group::new(vec![Box::new(Input::new().key("test").title("Test Input"))])
5602            .title("Group Title")
5603            .description("Group Description");
5604
5605        let header = group.header();
5606        assert!(header.contains("Group Title"));
5607        assert!(header.contains("Group Description"));
5608
5609        let content = group.content();
5610        assert!(content.contains("Test Input"));
5611
5612        let footer = group.footer();
5613        // No errors, so footer should be empty
5614        assert!(footer.is_empty());
5615    }
5616
5617    #[test]
5618    fn test_form_all_errors() {
5619        let form = Form::new(vec![Group::new(Vec::new())]);
5620
5621        // No errors initially
5622        let errors = form.all_errors();
5623        assert!(errors.is_empty());
5624    }
5625
5626    // Word transformation tests matching Go bubbles/textarea behavior
5627
5628    #[test]
5629    fn test_text_transpose_left() {
5630        let mut text = Text::new().value("hello");
5631        text.cursor_row = 0;
5632        text.cursor_col = 5; // At end of "hello"
5633
5634        text.transpose_left();
5635
5636        // At end, moves cursor back first, then swaps 'l' and 'o'
5637        assert_eq!(text.get_string_value(), "helol");
5638        assert_eq!(text.cursor_col, 5); // Cursor stays at end
5639    }
5640
5641    #[test]
5642    fn test_text_transpose_left_middle() {
5643        let mut text = Text::new().value("hello");
5644        text.cursor_row = 0;
5645        text.cursor_col = 2; // After 'e', before 'l'
5646
5647        text.transpose_left();
5648
5649        // Swaps 'e' (pos 1) and 'l' (pos 2)
5650        assert_eq!(text.get_string_value(), "hlelo");
5651        assert_eq!(text.cursor_col, 3); // Cursor moves right
5652    }
5653
5654    #[test]
5655    fn test_text_transpose_left_at_beginning() {
5656        let mut text = Text::new().value("hello");
5657        text.cursor_row = 0;
5658        text.cursor_col = 0; // At beginning
5659
5660        text.transpose_left();
5661
5662        // No-op when at beginning
5663        assert_eq!(text.get_string_value(), "hello");
5664        assert_eq!(text.cursor_col, 0);
5665    }
5666
5667    #[test]
5668    fn test_text_uppercase_right() {
5669        let mut text = Text::new().value("hello world");
5670        text.cursor_row = 0;
5671        text.cursor_col = 0; // At beginning
5672
5673        text.uppercase_right();
5674
5675        assert_eq!(text.get_string_value(), "HELLO world");
5676        assert_eq!(text.cursor_col, 5); // Cursor moves past the word
5677    }
5678
5679    #[test]
5680    fn test_text_uppercase_right_with_spaces() {
5681        let mut text = Text::new().value("  hello world");
5682        text.cursor_row = 0;
5683        text.cursor_col = 0; // Before spaces
5684
5685        text.uppercase_right();
5686
5687        // Skips spaces, then uppercases "hello"
5688        assert_eq!(text.get_string_value(), "  HELLO world");
5689        assert_eq!(text.cursor_col, 7); // Cursor after "HELLO"
5690    }
5691
5692    #[test]
5693    fn test_text_lowercase_right() {
5694        let mut text = Text::new().value("HELLO WORLD");
5695        text.cursor_row = 0;
5696        text.cursor_col = 0;
5697
5698        text.lowercase_right();
5699
5700        assert_eq!(text.get_string_value(), "hello WORLD");
5701        assert_eq!(text.cursor_col, 5);
5702    }
5703
5704    #[test]
5705    fn test_text_capitalize_right() {
5706        let mut text = Text::new().value("hello world");
5707        text.cursor_row = 0;
5708        text.cursor_col = 0;
5709
5710        text.capitalize_right();
5711
5712        // Only first char is uppercased
5713        assert_eq!(text.get_string_value(), "Hello world");
5714        assert_eq!(text.cursor_col, 5);
5715    }
5716
5717    #[test]
5718    fn test_text_capitalize_right_already_upper() {
5719        let mut text = Text::new().value("HELLO WORLD");
5720        text.cursor_row = 0;
5721        text.cursor_col = 0;
5722
5723        text.capitalize_right();
5724
5725        // First char stays upper, rest unchanged (capitalize doesn't lowercase)
5726        assert_eq!(text.get_string_value(), "HELLO WORLD");
5727        assert_eq!(text.cursor_col, 5);
5728    }
5729
5730    #[test]
5731    fn test_text_word_ops_multiline() {
5732        let mut text = Text::new().value("hello\nworld");
5733        text.cursor_row = 1;
5734        text.cursor_col = 0;
5735
5736        text.uppercase_right();
5737
5738        // Only operates on current line
5739        assert_eq!(text.get_string_value(), "hello\nWORLD");
5740        assert_eq!(text.cursor_row, 1);
5741        assert_eq!(text.cursor_col, 5);
5742    }
5743
5744    #[test]
5745    fn test_text_transpose_multiline() {
5746        let mut text = Text::new().value("ab\ncd");
5747        text.cursor_row = 1;
5748        text.cursor_col = 2; // At end of "cd"
5749
5750        text.transpose_left();
5751
5752        // Swaps 'c' and 'd' on second line
5753        assert_eq!(text.get_string_value(), "ab\ndc");
5754    }
5755
5756    #[test]
5757    fn test_text_word_ops_unicode() {
5758        let mut text = Text::new().value("café résumé");
5759        text.cursor_row = 0;
5760        text.cursor_col = 0;
5761
5762        text.uppercase_right();
5763
5764        assert_eq!(text.get_string_value(), "CAFÉ résumé");
5765        assert_eq!(text.cursor_col, 4);
5766    }
5767
5768    #[test]
5769    fn test_text_keymap_has_word_ops() {
5770        let keymap = TextKeyMap::default();
5771
5772        // Verify the new bindings exist and are enabled
5773        assert!(keymap.uppercase_word_forward.enabled());
5774        assert!(keymap.lowercase_word_forward.enabled());
5775        assert!(keymap.capitalize_word_forward.enabled());
5776        assert!(keymap.transpose_character_backward.enabled());
5777
5778        // Verify expected key bindings
5779        assert!(
5780            keymap
5781                .uppercase_word_forward
5782                .get_keys()
5783                .contains(&"alt+u".to_string())
5784        );
5785        assert!(
5786            keymap
5787                .lowercase_word_forward
5788                .get_keys()
5789                .contains(&"alt+l".to_string())
5790        );
5791        assert!(
5792            keymap
5793                .capitalize_word_forward
5794                .get_keys()
5795                .contains(&"alt+c".to_string())
5796        );
5797        assert!(
5798            keymap
5799                .transpose_character_backward
5800                .get_keys()
5801                .contains(&"ctrl+t".to_string())
5802        );
5803    }
5804
5805    // -------------------------------------------------------------------------
5806    // Paste handling tests (bd-3jg2)
5807    // -------------------------------------------------------------------------
5808
5809    mod paste_tests {
5810        use super::*;
5811        use bubbletea::{KeyMsg, Message};
5812
5813        /// Helper to create a paste KeyMsg from a string
5814        fn paste_msg(s: &str) -> Message {
5815            let key = KeyMsg::from_runes(s.chars().collect()).with_paste();
5816            Message::new(key)
5817        }
5818
5819        /// Helper to create a regular typing KeyMsg from a string
5820        fn type_msg(s: &str) -> Message {
5821            let key = KeyMsg::from_runes(s.chars().collect());
5822            Message::new(key)
5823        }
5824
5825        #[test]
5826        fn test_input_paste_collapses_newlines() {
5827            let mut input = Input::new().key("query");
5828            input.focused = true;
5829
5830            // Paste multi-line content
5831            let msg = paste_msg("hello\nworld\nfoo");
5832            input.update(&msg);
5833
5834            // Newlines should be collapsed to spaces
5835            assert_eq!(input.get_string_value(), "hello world foo");
5836        }
5837
5838        #[test]
5839        fn test_input_paste_collapses_tabs() {
5840            let mut input = Input::new().key("query");
5841            input.focused = true;
5842
5843            // Paste content with tabs
5844            let msg = paste_msg("col1\tcol2\tcol3");
5845            input.update(&msg);
5846
5847            // Tabs should be collapsed to spaces
5848            assert_eq!(input.get_string_value(), "col1 col2 col3");
5849        }
5850
5851        #[test]
5852        fn test_input_paste_collapses_multiple_spaces() {
5853            let mut input = Input::new().key("query");
5854            input.focused = true;
5855
5856            // Paste content with multiple consecutive newlines/spaces
5857            let msg = paste_msg("hello\n\n\nworld");
5858            input.update(&msg);
5859
5860            // Multiple consecutive whitespace should collapse to single space
5861            assert_eq!(input.get_string_value(), "hello world");
5862        }
5863
5864        #[test]
5865        fn test_input_paste_respects_char_limit() {
5866            let mut input = Input::new().key("query").char_limit(10);
5867            input.focused = true;
5868
5869            // Paste more than char_limit
5870            let msg = paste_msg("hello world this is too long");
5871            input.update(&msg);
5872
5873            // Should be truncated at limit
5874            assert_eq!(input.get_string_value().chars().count(), 10);
5875            assert_eq!(input.get_string_value(), "hello worl");
5876        }
5877
5878        #[test]
5879        fn test_input_paste_partial_fill() {
5880            let mut input = Input::new().key("query").char_limit(15);
5881            input.focused = true;
5882
5883            // Type some chars first
5884            let msg = type_msg("hi ");
5885            input.update(&msg);
5886
5887            // Paste more - should fill up to limit
5888            let msg = paste_msg("hello world this is long");
5889            input.update(&msg);
5890
5891            assert_eq!(input.get_string_value().chars().count(), 15);
5892            assert_eq!(input.get_string_value(), "hi hello world ");
5893        }
5894
5895        #[test]
5896        fn test_input_paste_cursor_position() {
5897            let mut input = Input::new().key("query");
5898            input.focused = true;
5899
5900            // Paste some content
5901            let msg = paste_msg("hello world");
5902            input.update(&msg);
5903
5904            // Cursor should be at end
5905            assert_eq!(input.cursor_pos, 11);
5906        }
5907
5908        #[test]
5909        fn test_input_regular_typing_not_affected() {
5910            let mut input = Input::new().key("query");
5911            input.focused = true;
5912
5913            // Regular typing of newline (shouldn't happen but test defensive behavior)
5914            let msg = type_msg("hello\nworld");
5915            input.update(&msg);
5916
5917            // Regular typing should preserve newlines (they're just chars)
5918            assert_eq!(input.get_string_value(), "hello\nworld");
5919        }
5920
5921        #[test]
5922        fn test_text_paste_preserves_newlines() {
5923            let mut text = Text::new().key("bio");
5924            text.focused = true;
5925
5926            // Paste multi-line content
5927            let msg = paste_msg("line 1\nline 2\nline 3");
5928            text.update(&msg);
5929
5930            // Newlines should be preserved in Text field
5931            assert_eq!(text.get_string_value(), "line 1\nline 2\nline 3");
5932        }
5933
5934        #[test]
5935        fn test_text_paste_updates_cursor_row() {
5936            let mut text = Text::new().key("bio");
5937            text.focused = true;
5938
5939            // Paste multi-line content
5940            let msg = paste_msg("line 1\nline 2\nline 3");
5941            text.update(&msg);
5942
5943            // Cursor should be on line 3 (0-indexed = 2)
5944            assert_eq!(text.cursor_row, 2);
5945            // Cursor col should be at end of "line 3"
5946            assert_eq!(text.cursor_col, 6);
5947        }
5948
5949        #[test]
5950        fn test_text_paste_respects_char_limit() {
5951            let mut text = Text::new().key("bio").char_limit(20);
5952            text.focused = true;
5953
5954            // Paste content exceeding limit
5955            let msg = paste_msg("line 1\nline 2\nline 3 is very long");
5956            text.update(&msg);
5957
5958            // Should truncate at 20 chars
5959            assert_eq!(text.get_string_value().chars().count(), 20);
5960        }
5961
5962        #[test]
5963        fn test_input_paste_unicode() {
5964            let mut input = Input::new().key("query");
5965            input.focused = true;
5966
5967            // Paste unicode content with newlines
5968            let msg = paste_msg("héllo\nwörld\n日本語");
5969            input.update(&msg);
5970
5971            // Should collapse newlines, preserve unicode
5972            assert_eq!(input.get_string_value(), "héllo wörld 日本語");
5973        }
5974
5975        #[test]
5976        fn test_text_paste_unicode_cursor() {
5977            let mut text = Text::new().key("bio");
5978            text.focused = true;
5979
5980            // Paste unicode content
5981            let msg = paste_msg("日本語\n한국어");
5982            text.update(&msg);
5983
5984            assert_eq!(text.get_string_value(), "日本語\n한국어");
5985            assert_eq!(text.cursor_row, 1);
5986            assert_eq!(text.cursor_col, 3); // 3 Korean chars
5987        }
5988
5989        #[test]
5990        fn test_input_paste_empty() {
5991            let mut input = Input::new().key("query");
5992            input.focused = true;
5993
5994            // Paste empty content
5995            let msg = paste_msg("");
5996            input.update(&msg);
5997
5998            assert_eq!(input.get_string_value(), "");
5999            assert_eq!(input.cursor_pos, 0);
6000        }
6001
6002        #[test]
6003        fn test_input_paste_crlf_handling() {
6004            let mut input = Input::new().key("query");
6005            input.focused = true;
6006
6007            // Paste Windows-style line endings
6008            let msg = paste_msg("hello\r\nworld");
6009            input.update(&msg);
6010
6011            // Both \r and \n should become spaces, then collapse
6012            assert_eq!(input.get_string_value(), "hello world");
6013        }
6014
6015        #[test]
6016        fn test_input_not_focused_ignores_paste() {
6017            let mut input = Input::new().key("query");
6018            input.focused = false;
6019
6020            let msg = paste_msg("hello world");
6021            input.update(&msg);
6022
6023            // Should ignore paste when not focused
6024            assert_eq!(input.get_string_value(), "");
6025        }
6026
6027        #[test]
6028        fn test_text_not_focused_ignores_paste() {
6029            let mut text = Text::new().key("bio");
6030            text.focused = false;
6031
6032            let msg = paste_msg("hello\nworld");
6033            text.update(&msg);
6034
6035            // Should ignore paste when not focused
6036            assert_eq!(text.get_string_value(), "");
6037        }
6038
6039        #[test]
6040        fn test_input_large_paste() {
6041            let mut input = Input::new().key("query");
6042            input.focused = true;
6043
6044            // Paste a large amount of text (simulating a real paste operation)
6045            let large_text: String = (0..1000).map(|i| format!("word{} ", i)).collect();
6046            let msg = paste_msg(&large_text);
6047            input.update(&msg);
6048
6049            // Should handle large paste without panic
6050            assert!(input.get_string_value().chars().count() > 100);
6051        }
6052
6053        #[test]
6054        fn test_text_large_paste() {
6055            let mut text = Text::new().key("bio");
6056            text.focused = true;
6057
6058            // Paste large multi-line text
6059            let large_text: String = (0..100).map(|i| format!("line {}\n", i)).collect();
6060            let msg = paste_msg(&large_text);
6061            text.update(&msg);
6062
6063            // Should handle large paste without panic
6064            assert!(text.get_string_value().contains('\n'));
6065            assert_eq!(text.cursor_row, 100); // 100 newlines = row 100
6066        }
6067    }
6068
6069    #[test]
6070    fn test_multiselect_filter_cursor_stays_on_item() {
6071        // Test that cursor stays on the same item when filter narrows results
6072        let mut multi: MultiSelect<String> = MultiSelect::new().filterable(true).options(vec![
6073            SelectOption::new("Apple", "apple".to_string()),
6074            SelectOption::new("Banana", "banana".to_string()),
6075            SelectOption::new("Cherry", "cherry".to_string()),
6076            SelectOption::new("Blueberry", "blueberry".to_string()),
6077        ]);
6078
6079        multi.focus();
6080
6081        // Move cursor to "Banana" (index 1)
6082        let down_msg = Message::new(KeyMsg {
6083            key_type: KeyType::Down,
6084            runes: vec![],
6085            alt: false,
6086            paste: false,
6087        });
6088        multi.update(&down_msg);
6089        assert_eq!(multi.cursor, 1);
6090
6091        // Apply filter "b" - should match Banana, Blueberry
6092        multi.update_filter("b".to_string());
6093
6094        // Cursor should still be on "Banana" which is now at filtered index 0
6095        let filtered = multi.filtered_options();
6096        assert_eq!(filtered.len(), 2);
6097        assert_eq!(filtered[multi.cursor].1.key, "Banana");
6098    }
6099
6100    #[test]
6101    fn test_multiselect_filter_cursor_clamps() {
6102        // Test that cursor clamps when the current item is filtered out
6103        let mut multi: MultiSelect<String> = MultiSelect::new().filterable(true).options(vec![
6104            SelectOption::new("Apple", "apple".to_string()),
6105            SelectOption::new("Banana", "banana".to_string()),
6106            SelectOption::new("Cherry", "cherry".to_string()),
6107        ]);
6108
6109        multi.focus();
6110
6111        // Move cursor to "Cherry" (index 2)
6112        let down_msg = Message::new(KeyMsg {
6113            key_type: KeyType::Down,
6114            runes: vec![],
6115            alt: false,
6116            paste: false,
6117        });
6118        multi.update(&down_msg);
6119        multi.update(&down_msg);
6120        assert_eq!(multi.cursor, 2);
6121
6122        // Apply filter "a" - should match Apple, Banana (not Cherry)
6123        multi.update_filter("a".to_string());
6124
6125        // Cursor should be clamped to valid range (max index 1)
6126        let filtered = multi.filtered_options();
6127        assert_eq!(filtered.len(), 2);
6128        assert!(multi.cursor < filtered.len());
6129    }
6130
6131    #[test]
6132    fn test_multiselect_filter_then_toggle() {
6133        // Test that toggling selection works correctly with filtered results
6134        let mut multi: MultiSelect<String> = MultiSelect::new().filterable(true).options(vec![
6135            SelectOption::new("Apple", "apple".to_string()),
6136            SelectOption::new("Banana", "banana".to_string()),
6137            SelectOption::new("Cherry", "cherry".to_string()),
6138            SelectOption::new("Blueberry", "blueberry".to_string()),
6139        ]);
6140
6141        multi.focus();
6142
6143        // Apply filter "b" - should match Banana, Blueberry
6144        multi.update_filter("b".to_string());
6145
6146        // Move to second item (Blueberry)
6147        let down_msg = Message::new(KeyMsg {
6148            key_type: KeyType::Down,
6149            runes: vec![],
6150            alt: false,
6151            paste: false,
6152        });
6153        multi.update(&down_msg);
6154
6155        // Toggle selection
6156        let toggle_msg = Message::new(KeyMsg {
6157            key_type: KeyType::Runes,
6158            runes: vec![' '],
6159            alt: false,
6160            paste: false,
6161        });
6162        multi.update(&toggle_msg);
6163
6164        // Verify Blueberry (original index 3) is selected
6165        let selected = multi.get_selected_values();
6166        assert_eq!(selected.len(), 1);
6167        assert!(selected.contains(&&"blueberry".to_string()));
6168
6169        // Clear filter and verify selection persists
6170        multi.update_filter(String::new());
6171        let selected = multi.get_selected_values();
6172        assert_eq!(selected.len(), 1);
6173        assert!(selected.contains(&&"blueberry".to_string()));
6174    }
6175
6176    #[test]
6177    fn test_multiselect_filter_navigation_bounds() {
6178        // Test that navigation respects filtered list bounds
6179        let mut multi: MultiSelect<String> = MultiSelect::new().filterable(true).options(vec![
6180            SelectOption::new("Apple", "apple".to_string()),
6181            SelectOption::new("Banana", "banana".to_string()),
6182            SelectOption::new("Cherry", "cherry".to_string()),
6183            SelectOption::new("Date", "date".to_string()),
6184        ]);
6185
6186        multi.focus();
6187
6188        // Apply filter "a" - should match Apple, Banana, Date (3 items)
6189        multi.update_filter("a".to_string());
6190        let filtered = multi.filtered_options();
6191        assert_eq!(filtered.len(), 3);
6192
6193        // Navigate down past the filtered list size
6194        let down_msg = Message::new(KeyMsg {
6195            key_type: KeyType::Down,
6196            runes: vec![],
6197            alt: false,
6198            paste: false,
6199        });
6200        multi.update(&down_msg);
6201        multi.update(&down_msg);
6202        multi.update(&down_msg); // Try to go past the end
6203        multi.update(&down_msg);
6204
6205        // Cursor should be capped at last filtered index
6206        assert_eq!(multi.cursor, 2); // Max index is 2 (3 items: 0, 1, 2)
6207    }
6208
6209    // -------------------------------------------------------------------------
6210    // FilePicker edge case tests (bd-1isw)
6211    // -------------------------------------------------------------------------
6212
6213    /// Helper to create a FilePicker pre-loaded with synthetic FileEntry items
6214    /// (avoids filesystem I/O in unit tests).
6215    fn filepicker_with_entries(entries: Vec<(&str, bool)>) -> FilePicker {
6216        let mut picker = FilePicker::new();
6217        picker.picking = true;
6218        picker.focused = true;
6219        picker.files = entries
6220            .into_iter()
6221            .map(|(name, is_dir)| FileEntry {
6222                name: name.to_string(),
6223                path: format!("/tmp/{name}"),
6224                is_dir,
6225                size: 0,
6226                mode: String::new(),
6227            })
6228            .collect();
6229        picker
6230    }
6231
6232    fn make_key_msg(key_type: KeyType) -> Message {
6233        Message::new(KeyMsg {
6234            key_type,
6235            runes: vec![],
6236            alt: false,
6237            paste: false,
6238        })
6239    }
6240
6241    #[test]
6242    fn filepicker_single_file_is_selected_by_default() {
6243        let picker = filepicker_with_entries(vec![("only_file.txt", false)]);
6244        // selected_index defaults to 0, which points at the only file
6245        assert_eq!(picker.selected_index, 0);
6246        assert_eq!(picker.files.len(), 1);
6247        assert_eq!(picker.files[0].name, "only_file.txt");
6248    }
6249
6250    #[test]
6251    fn filepicker_single_file_view_shows_entry() {
6252        let picker = filepicker_with_entries(vec![("only_file.txt", false)]);
6253        let view = picker.view();
6254        assert!(view.contains("only_file.txt"));
6255    }
6256
6257    #[test]
6258    fn filepicker_single_file_select_via_enter() {
6259        let mut picker = filepicker_with_entries(vec![("report.pdf", false)]);
6260        // Simulate pressing Enter (open binding)
6261        let enter_msg = make_key_msg(KeyType::Enter);
6262        let result = picker.update(&enter_msg);
6263        // Should select the file and advance
6264        assert_eq!(picker.selected_path, Some("/tmp/report.pdf".to_string()));
6265        assert!(!picker.picking);
6266        assert!(result.is_some()); // NextFieldMsg command returned
6267    }
6268
6269    #[test]
6270    fn filepicker_single_file_down_does_not_move() {
6271        let mut picker = filepicker_with_entries(vec![("only.txt", false)]);
6272        let down_msg = make_key_msg(KeyType::Down);
6273        picker.update(&down_msg);
6274        // Should remain at index 0 - nowhere to go
6275        assert_eq!(picker.selected_index, 0);
6276    }
6277
6278    #[test]
6279    fn filepicker_single_file_up_does_not_move() {
6280        let mut picker = filepicker_with_entries(vec![("only.txt", false)]);
6281        let up_msg = make_key_msg(KeyType::Up);
6282        picker.update(&up_msg);
6283        assert_eq!(picker.selected_index, 0);
6284    }
6285
6286    #[test]
6287    fn filepicker_empty_files_no_panic() {
6288        let mut picker = filepicker_with_entries(vec![]);
6289        // Verify no panic on navigation with empty list
6290        let down_msg = make_key_msg(KeyType::Down);
6291        picker.update(&down_msg);
6292        assert_eq!(picker.selected_index, 0);
6293
6294        let up_msg = make_key_msg(KeyType::Up);
6295        picker.update(&up_msg);
6296        assert_eq!(picker.selected_index, 0);
6297    }
6298
6299    #[test]
6300    fn filepicker_empty_files_view_no_panic() {
6301        let picker = filepicker_with_entries(vec![]);
6302        // Should render without panic even with no files
6303        let view = picker.view();
6304        assert!(!view.is_empty());
6305    }
6306
6307    #[test]
6308    fn filepicker_empty_goto_top_bottom_no_panic() {
6309        let mut picker = filepicker_with_entries(vec![]);
6310        // goto_top
6311        let home_msg = Message::new(KeyMsg {
6312            key_type: KeyType::Home,
6313            runes: vec![],
6314            alt: false,
6315            paste: false,
6316        });
6317        picker.update(&home_msg);
6318        assert_eq!(picker.selected_index, 0);
6319
6320        // goto_bottom
6321        let end_msg = Message::new(KeyMsg {
6322            key_type: KeyType::End,
6323            runes: vec![],
6324            alt: false,
6325            paste: false,
6326        });
6327        picker.update(&end_msg);
6328        assert_eq!(picker.selected_index, 0);
6329    }
6330
6331    #[test]
6332    fn filepicker_height_zero_no_panic() {
6333        let mut picker =
6334            filepicker_with_entries(vec![("a.txt", false), ("b.txt", false), ("c.txt", false)]);
6335        picker.height = 0;
6336        // Navigate down — must not panic on offset calculation
6337        let down_msg = make_key_msg(KeyType::Down);
6338        picker.update(&down_msg);
6339        picker.update(&down_msg);
6340        assert_eq!(picker.selected_index, 2);
6341    }
6342
6343    #[test]
6344    fn filepicker_height_one_scrolls_correctly() {
6345        let mut picker =
6346            filepicker_with_entries(vec![("a.txt", false), ("b.txt", false), ("c.txt", false)]);
6347        picker.height = 1;
6348        assert_eq!(picker.selected_index, 0);
6349        assert_eq!(picker.offset, 0);
6350
6351        let down_msg = make_key_msg(KeyType::Down);
6352        picker.update(&down_msg);
6353        assert_eq!(picker.selected_index, 1);
6354        // With height=1, offset should scroll to keep selected visible
6355        assert_eq!(picker.offset, 1);
6356
6357        picker.update(&down_msg);
6358        assert_eq!(picker.selected_index, 2);
6359        assert_eq!(picker.offset, 2);
6360    }
6361
6362    #[test]
6363    fn filepicker_navigation_respects_bounds() {
6364        let mut picker = filepicker_with_entries(vec![("a.txt", false), ("b.txt", false)]);
6365        let down_msg = make_key_msg(KeyType::Down);
6366        let up_msg = make_key_msg(KeyType::Up);
6367
6368        // Navigate down past end
6369        picker.update(&down_msg);
6370        assert_eq!(picker.selected_index, 1);
6371        picker.update(&down_msg); // Should stay at 1
6372        assert_eq!(picker.selected_index, 1);
6373
6374        // Navigate up past start
6375        picker.update(&up_msg);
6376        assert_eq!(picker.selected_index, 0);
6377        picker.update(&up_msg); // Should stay at 0
6378        assert_eq!(picker.selected_index, 0);
6379    }
6380
6381    #[test]
6382    fn filepicker_dir_not_selectable_by_default() {
6383        let picker = filepicker_with_entries(vec![("subdir", true)]);
6384        let entry = &picker.files[0];
6385        // By default, dir_allowed is false
6386        assert!(!picker.is_selectable(entry));
6387    }
6388
6389    #[test]
6390    fn filepicker_file_selectable_by_default() {
6391        let picker = filepicker_with_entries(vec![("file.rs", false)]);
6392        let entry = &picker.files[0];
6393        assert!(picker.is_selectable(entry));
6394    }
6395
6396    #[test]
6397    fn filepicker_format_size_edge_cases() {
6398        assert_eq!(FilePicker::format_size(0), "0B");
6399        assert_eq!(FilePicker::format_size(1023), "1023B");
6400        assert_eq!(FilePicker::format_size(1024), "1.0K");
6401        assert_eq!(FilePicker::format_size(1024 * 1024), "1.0M");
6402        assert_eq!(FilePicker::format_size(1024 * 1024 * 1024), "1.0G");
6403    }
6404
6405    // ---- Select filter tests ----
6406
6407    fn make_select_options() -> Vec<SelectOption<String>> {
6408        vec![
6409            SelectOption::new("Apple", "apple".to_string()),
6410            SelectOption::new("Apricot", "apricot".to_string()),
6411            SelectOption::new("Banana", "banana".to_string()),
6412            SelectOption::new("Cherry", "cherry".to_string()),
6413            SelectOption::new("Date", "date".to_string()),
6414        ]
6415    }
6416
6417    fn make_filterable_select() -> Select<String> {
6418        Select::new()
6419            .options(make_select_options())
6420            .filterable(true)
6421            .height_options(3)
6422    }
6423
6424    #[test]
6425    fn select_filterable_builder() {
6426        let sel = Select::<String>::new().filterable(true);
6427        assert!(sel.filtering);
6428        let sel = Select::<String>::new().filterable(false);
6429        assert!(!sel.filtering);
6430    }
6431
6432    #[test]
6433    fn select_filtered_indices_no_filter() {
6434        let sel = make_filterable_select();
6435        assert_eq!(sel.filtered_indices(), vec![0, 1, 2, 3, 4]);
6436    }
6437
6438    #[test]
6439    fn select_filtered_indices_with_filter() {
6440        let mut sel = make_filterable_select();
6441        sel.filter_value = "ap".to_string();
6442        // "Apple" and "Apricot" match "ap"
6443        assert_eq!(sel.filtered_indices(), vec![0, 1]);
6444    }
6445
6446    #[test]
6447    fn select_filtered_indices_case_insensitive() {
6448        let mut sel = make_filterable_select();
6449        sel.filter_value = "AP".to_string();
6450        assert_eq!(sel.filtered_indices(), vec![0, 1]);
6451    }
6452
6453    #[test]
6454    fn select_filtered_indices_no_match() {
6455        let mut sel = make_filterable_select();
6456        sel.filter_value = "zzz".to_string();
6457        assert!(sel.filtered_indices().is_empty());
6458    }
6459
6460    #[test]
6461    fn select_update_filter_keeps_selection() {
6462        let mut sel = make_filterable_select();
6463        sel.selected = 2; // Banana
6464        sel.update_filter("an".to_string());
6465        // "Banana" contains "an" — should still be selected
6466        assert_eq!(sel.selected, 2);
6467        assert_eq!(sel.filter_value, "an");
6468    }
6469
6470    #[test]
6471    fn select_update_filter_clamps_when_item_hidden() {
6472        let mut sel = make_filterable_select();
6473        sel.selected = 2; // Banana
6474        sel.update_filter("ch".to_string());
6475        // Only "Cherry" matches "ch" — Banana hidden
6476        // selected should move to Cherry (index 3)
6477        assert_eq!(sel.selected, 3);
6478    }
6479
6480    #[test]
6481    fn select_update_filter_clear_restores() {
6482        let mut sel = make_filterable_select();
6483        sel.update_filter("ap".to_string());
6484        assert_eq!(sel.filtered_indices(), vec![0, 1]);
6485        sel.update_filter(String::new());
6486        assert_eq!(sel.filtered_indices(), vec![0, 1, 2, 3, 4]);
6487    }
6488
6489    #[test]
6490    fn select_filter_display_in_view() {
6491        let mut sel = make_filterable_select();
6492        sel.focused = true;
6493        sel.filter_value = "ap".to_string();
6494        let view = sel.view();
6495        assert!(view.contains("Filter: ap_"));
6496    }
6497
6498    #[test]
6499    fn select_filter_not_displayed_when_empty() {
6500        let mut sel = make_filterable_select();
6501        sel.focused = true;
6502        let view = sel.view();
6503        assert!(!view.contains("Filter:"));
6504    }
6505
6506    #[test]
6507    fn select_filter_not_displayed_when_disabled() {
6508        let mut sel = Select::new()
6509            .options(make_select_options())
6510            .height_options(3);
6511        sel.focused = true;
6512        sel.filter_value = "ap".to_string();
6513        let view = sel.view();
6514        assert!(!view.contains("Filter:"));
6515    }
6516
6517    #[test]
6518    fn select_navigation_respects_filter() {
6519        let mut sel = make_filterable_select();
6520        sel.focused = true;
6521        sel.update_filter("a".to_string());
6522        // Matches: Apple(0), Apricot(1), Banana(2), Date(4)
6523        let indices = sel.filtered_indices();
6524        assert_eq!(indices, vec![0, 1, 2, 4]);
6525
6526        // selected should be 0 (Apple)
6527        sel.selected = 0;
6528
6529        // Create a "down" key message
6530        let down_msg = Message::new(KeyMsg {
6531            key_type: KeyType::Down,
6532            runes: vec![],
6533            alt: false,
6534            paste: false,
6535        });
6536        sel.update(&down_msg);
6537        // Should move to next in filtered list: Apricot (1)
6538        assert_eq!(sel.selected, 1);
6539
6540        sel.update(&down_msg);
6541        // Should move to Banana (2)
6542        assert_eq!(sel.selected, 2);
6543
6544        sel.update(&down_msg);
6545        // Should move to Date (4), skipping Cherry (3) which doesn't match
6546        assert_eq!(sel.selected, 4);
6547    }
6548
6549    #[test]
6550    fn select_get_selected_value_with_filter() {
6551        let mut sel = make_filterable_select();
6552        sel.update_filter("ch".to_string());
6553        // Only Cherry matches, selected should be 3
6554        assert_eq!(sel.selected, 3);
6555        assert_eq!(sel.get_selected_value(), Some(&"cherry".to_string()));
6556    }
6557}